一、进程、线程
概念 | 调度 | 创建和切换代价 | 组成 | 备注 |
---|---|---|---|---|
进程 | 操作系统 | 极高 | 资源分配的最小单位 | |
线程 | 操作系统 | 高 | 进程的组成部分 | CPU调度和执行的最小单位 |
协程 | 应用程序 | 低 | 线程的组成部门 |
理解并发与并行
- 并行:在某一时刻任务A和任务B同时执行,多任务在多核心场景下可能发生
- 并发:在任务A和任务B的生命周期内存在时间重叠,单核心也可能发生
二、线程的上下文切换
步骤 | 说明 |
---|---|
从操作系统用户态切换到内核态 | 记录上一个线程的重要寄存器值、进程状态等信息 |
切换到下一个要执行的线程,并从内核态转移到操作系统用户态 | 重新加载重要的CPU寄存器值 |
如果线程在上下文切换时属于不同的进程,需要更新额外的状态信息及内存地址空间(进程切换的代价比线程切换代价更大的原因)。
三、线程、协程
内核态的上下文切换存在大量的任务外数据交换,基于此协程出现,也有地方称为“微线程”
线程 | 协程 | |
---|---|---|
调度方式 | 由CPU统一调度管理 | 由应用程序自行管理 |
上下文切换 | 线程切换大于1~2微秒 | Go语言中协程切换为0.2微秒左右 |
调度策略 | 抢占式,操作系统定时中断并执行上下文切换 | Go语言中为协作式,协程执行完主动将执行权让给其他 |
栈的大小 | 一般在创建时指定,为避免栈溢出默认的栈相对较大,例如2MB | Go语言的协程栈默认为2KB |
四、Java与Go中的协程
4.1、协程简介
原生支持协程的编程语言包括:C++20、Golang、Python等,Java也有诸如quasar等三方框架支持协程,Java19开始原生支持协程
Java | Go | |
---|---|---|
线程 | 用户自行管理 | Go运行时管理 |
协程 | 用户自行管理 | 用户自行管理 |
Go语言原生支持协程,且没有线程的概念,由运行时根据配置决定线程(实际任务执行单元)的数量
// Go运行一个协程
go func() {
fmt.Println("hello goroutine")
}()
Java19中原生支持协程,新增VirtualThread类
// 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池化线程和池化协程
// 一、创建线程的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
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
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,执行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
单个 | Cached池 | Fixed池 | PerThread池 | Scheduled池 | Single池 | Single Scheduled池 | Go | |
线程1000/100 | 0s|115088 (133) | 0s|79516 (337) | 0s|130177 (100) | 0s|125051 (1000) | 0s|127246 (100) | 11s|250205 (1) | 11s|364726 (1) | |
协程 1000/100 | 0s|49822 (841) | 0s|61327 (1000) | 0s|119982 (100) | 0s|18093 (147) | 0s|129003 (100) | 11s|307560 (1) | 11s|672096 (1) | 0s|3033 (948) |
线程1000/50 | 0s|113081 (137) | 0s|67799 (385) | 0s|251384 (50) | 0s|123449 (145) | 0s|243561(50) | 11s|490609 (1) | 11s|561867 (1) | |
协程 1000/50 | 0s|60231 (664) | 0s|55655 (915) | 0s|242863(50) | 0s|27372 (1000) | 0s|239014(50) | 11s|327316 (1) | 11s|619707 (1) | 0s|3283 (948) |
线程2000/50 | 0s|224622 (143) | 0s|162239 (501) | 0s|476044(50) | 0s|208582 (140) | 0s|479096(50) | 22s|898969 (1) | 23s|35070 (1) | |
协程 2000/50 | 0s|77850 (1192) | 0s|42432 (1652) | 0s|492283(50) | 0s|17967 (2000) | 0s|475407(50) | 22s|977343 (1) | 23s|79873 (1) | 0s|6043 (1948) |
各种方式的总结
管理方式 | 特点 |
---|---|
单个 | 任意创建,但线程相比协程创建的代价大 |
Cached池 | 会复用部分线程,相比单个会有提升,数量越多提升越明显 |
Fixed池 | 协程和线程都固定数量,基本相同 |
PerThread池 | 类似单个创建的方式 |
Scheduled池 | 受限于核心线程数 |
Single池 | 单线程或单协程处理,基本相同 |
SingleScheduled池 | 单线程或单协程处理,基本相同 |
五、拓展阅读
- Java低版本有三方框架支持协程方式,例如quasar:参见Java之协程(quasar)
- openjdk的早期测试版可通过https://jdk.java.net/ 下载体验,相关核心版本的java源码可参考ns-cn/jdk: jdk (github.com)
- 一种在idea中使用自己的Java源码调试的方法
如有需要可使用个人提供的源码项目,提取自zulu-jdk,项目地址:GitHub - ns-cn/jdk: jdk
六、附录(本次演示测试结果详情)
演示代码仓库:GitHub - ns-cn/JavaVirtualThreadVSGoroutine: Java协程、线程和Go协程的对比