Java 19[线程×,协程√]

一、进程、线程、协程

表1 进程、线程和协程一览

概念 调度 创建和切换代价 组成 备注
进程 操作系统 极高 资源调度的最小单元
线程 操作系统 进程的组成部分 CPU调度和执行的最小单位
协程 应用程序 线程的组成部门

理解并发与并行

  • 并行:在某一时刻任务A和任务B同时执行,多任务在多核心场景下可能发生
  • 并发:在任务A和任务B的生命周期内存在时间重叠,单核心也可能发生

二、线程的上下文切换

表2 线程上下文切换过程

步骤 说明
从操作系统用户态切换到内核态 记录上一个线程的重要寄存器值、进程状态等信息
切换到下一个要执行的线程,并从内核态转移到操作系统用户态 重新加载重要的CPU寄存器值

如果线程在上下文切换时属于不同的进程,需要更新额外的状态信息及内存地址空间(进程切换的代价比线程切换代价更大的原因)。

三、协程、协程

内核态的上下文切换存在大量的任务外数据交换,基于此协程出现,也有地方称为“微线程” 表3 Go语言中线程和协程对比

线程 协程
调度方式 由CPU统一调度管理 由应用程序自行管理
上下文切换 线程切换大于1~2微秒 Go语言中协程切换为0.2微秒左右
调度策略 抢占式,操作系统定时中断并执行上下文切换 Go语言中为协作式,协程执行完主动将执行权让给其他
栈的大小 一般在创建时指定,为避免栈溢出默认的栈相对较大,例如2MB Go语言的协程栈默认为2KB

四、Java与Go中的协程

4.1、协程简介

原生支持协程的编程语言包括:C++20、Golang、Python等,Java也有诸如_quasar等_三方框架支持协程,Java19开始原生支持协程 表4 Java与Go的线程与协程对比

Java Go
线程 用户自行管理 Go运行时管理
协程 用户自行管理 用户自行管理

Go语言原生支持协程,且没有线程的概念,由运行时根据配置决定线程(实际任务执行单元)的数量

1
2
3
4
// Go运行一个协程
go func() {
    fmt.Println("hello goroutine")
}()

Java19中原生支持协程,新增VirtualThread类

1
2
3
4
5
// Java19通过Thread类静态方法启动一个线程,实际类型为Thread
Thread platformThread = Thread.ofPlatform().name("myPlatformThread").start(() -> System.out.println("hello platform thread"));

// Java19通过Thread类静态方法启动一个协程, 实际类型为VirtualThread
Thread virtualThread = Thread.ofVirtual().name("myVirtualThread").start(() -> System.out.println("hello virtual thread"));

Java19池化线程和池化协程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 一、创建线程的ThreadFactory或则协程的ThreadFactory
ThreadFactory virtualFactory = Thread.ofVirtual().factory();
ThreadFactory platformFactory = Thread.ofPlatform().factory();
 
// 一、创建线程池或协程池
// 2.1、通过ThreadPoolExecutor带ThreadFactory参数的构造函数方式创建线程池或则协程池
new ThreadPoolExecutor(100, 1000, 60, TimeUnit.Second,
    new LinkedBlockingQueue<Runnable>(),
    virtualFactory, 
    (r, executor) -> System.out.println("塞不下了"));
 
// 2.2、通过Executors的静态方法创建,例如创建单一线程的线程池或单一协程的协程池
ExecutorService virtualExecutorService = Executors.newSingleThreadExecutor(virtualFactory);
ExecutorService platformExecutorService = Executors.newSingleThreadExecutor(platformFactory);

4.2、效率对比

本示例通过使用Java线程、协程和Go协程执行相同的代码功能做简单的耗时对比,任务内容为休眠10毫秒模拟任务,并使用并发计数器在任务开始和结束记录数量,使用任务数量计数器在任务结束时记录完成的任务数方便主程序退出。相关的完整源代码参见:https://github.com/ns-cn/JavaVirtualThreadVSGoroutine/ Java语言的任务代码,完整代码:Main.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
AtomicInteger count = new AtomicInteger(0);
AtomicInteger nowInUse = new AtomicInteger(0);
AtomicInteger maxInUse = new AtomicInteger(0);
Runnable task = () -> {
    int[] stoarge = new int[1024];
    nowInUse.incrementAndGet();
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    if (nowInUse.get() > maxInUse.get()) {
        maxInUse.set(nowInUse.get());
    }
    count.incrementAndGet();
    nowInUse.decrementAndGet();
};

Go语言的任务代码,完整代码:main.go

1
2
3
4
5
6
7
8
9
var startTime = time.Now()
var nowInUseChan = make(chan int, 100)
task := func() {
    nowInUseChan <- 1
    defer func() {
        nowInUseChan <- -1
    }()
    time.Sleep(10)
}

执行对比脚本,其中调用参数

  • threads: 标识执行的总任务数,
  • type: 标识管理任务的类型,如果只以单一池化方式则为0000010,整数为4
  • coreSize:标识池化方式的核心线程数的数量

响应结果,其中

  • isVirtual:标识是否是协程方式
  • final:表示执行结果,前面为秒的粗略统计方便长时间时的标识,后面为微秒
  • maxInUse:表示执行结果中最大的并行执行任务数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 测试样例1,执行2000次任务,全部执行,池化方式50核心线程数
➜  virtual git:(main) ✗ ./runMain.sh 2000 $((2#1111111)) 50
threads: 2000 type: 127 coreSize: 50
JAVA 开始
注: Main.java 使用 Java SE 19 的预览功能。
注: 有关详细信息,请使用 -Xlint:preview 重新编译。
threads: 2000   type: 1111111   coreSize: 50
-----------------THREAD-----------------
isVirtual: Y    final: 0s|77850 maxInUse: 1192
isVirtual: N    final: 0s|224622        maxInUse: 143
-----------------POOL_CACHED-----------------
isVirtual: Y    final: 0s|42432 maxInUse: 1652
isVirtual: N    final: 0s|162239        maxInUse: 501
-----------------POOL_FIXED-----------------
isVirtual: Y    final: 0s|492283        maxInUse: 50
isVirtual: N    final: 0s|476044        maxInUse: 50
-----------------POOL_PER-----------------
isVirtual: Y    final: 0s|17967 maxInUse: 2000
isVirtual: N    final: 0s|208582        maxInUse: 140
-----------------POOL_SCHEDULED-----------------
isVirtual: Y    final: 0s|475407        maxInUse: 50
isVirtual: N    final: 0s|479096        maxInUse: 50
-----------------POOL_SINGLE-----------------
isVirtual: Y    final: 22s|977343       maxInUse: 1
isVirtual: N    final: 22s|898969       maxInUse: 1
-----------------POOL_SINGLE_SCHEDULED-----------------
isVirtual: Y    final: 23s|79873        maxInUse: 1
isVirtual: N    final: 23s|35070        maxInUse: 1
GO 开始
-----------------GO Routine-----------------
isVirtual: Y    final: 0s|6043  maxInUse: 1948

格式说明:

  • 类型:【类型】【总的任务数】/【核心线程数】
  • 执行结果:【执行时长,单位秒】|【执行时长,微秒】(【最大并行数量】)

表5 各种方式总结

特点
单个 任意创建,但线程相比协程创建的代价大
Cached池 会复用部分线程,相比单个会有提升,数量越多提升越明显
Fixed池 协程和线程都固定数量,基本相同
PerThread池 类似单个创建的方式
Scheduled池 受限于核心线程数
Single池 单线程或单协程处理,基本相同
SingleScheduled池 单线程或单协程处理,基本相同

五、拓展阅读

  1. Java低版本有三方框架支持协程方式,例如quasar:参见Java之协程(quasar)
  2. openjdk的早期测试版可通过https://jdk.java.net/ 下载体验,相关核心版本的java源码可参考ns-cn/jdk: jdk (github.com)
  3. 一种在idea中使用自己的Java源码调试的方法

如有需要可使用个人提供的源码项目,提取自zulu-jdk,项目地址:GitHub - ns-cn/jdk: jdk

六、附录(本次演示测试结果详情)

演示代码仓库:GitHub - ns-cn/JavaVirtualThreadVSGoroutine: Java协程、线程和Go协程的对比 演示结果样例:JavaVirtualThreadVSGoroutine/对比结果.md

0%