前言
Java 21里正式发布了java的协程(Java17作为预览功能有提供),内部叫虚拟线程,参考:https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-15BDB995-028A-45A7-B6E2-9BA15C2E0501 本文是Java协程和线程调用的简单性能对比,以及GO协程的简单介绍。
原理
下面直接对Java虚拟线程直接叫协程,关键原理:
-
Java协程的调度在用户空间进行,而不依赖操作系统内核,它的创建销毁和上下文切换的开销更低,且多个协程映射到少量的平台线程(操作系统的线程)上。
-
采用协作式多任务模型。当一个协程阻塞在 I/O 操作或其他等待状态时,它会主动释放执行权,允许其他协程在同一个平台线程上运行,而不是像传统线程那样被操作系统强制挂起。
GO语言主要优势是高性能高并发,一个关键点是提供了 goroutine的协程并发能力,基于GMP调度模型实现了协程能力:
• G:表示goroutine,goroutine是Go语言中的协程。
• M:抽象了内核线程,代表操作系统的线程,用于执行goroutines,当goroutine 调度到线程时,使用该goroutine 自己的栈信息。
• P:代表处理器,负责调度goroutine,每个P维护一个本地goroutine 队列,M 从P 上获得goroutine 并执行。P还负责从全局队列中获取新的任务。
性能对比
下面直接看代码执行结果。
JAVA比较协程和线程循环1万次方法,每次方法内部休眠500毫秒的调用:
public class VirtualThreadTest {
static List<Integer> list = new ArrayList<>();
static int size = 10000;
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
updateMaxThreadNum(threadInfo.length);
}, 10, 10, TimeUnit.MILLISECONDS);
testVirtualThread();
}
public static void testVirtualThread() throws InterruptedException {
list = new ArrayList<>();
Thread.sleep(200);
System.out.println("start:" + list.get(0) + " platform threads");
long start = System.currentTimeMillis();
// 1.虚拟线程
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 2.平台线程
// ExecutorService executor = Executors.newFixedThreadPool(500);
CountDownLatch countDownLatch = new CountDownLatch(size);
for (int i = 0; i < size; i++) {
executor.submit(() -> {
try {
// 睡眠500毫秒
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException ex) {
System.out.println(ex);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executor.close();
System.out.println("max:" + list.get(0) + " platform threads");
System.out.printf("cost:%dms\n", System.currentTimeMillis() - start);
}
private static void updateMaxThreadNum(int num) {
if (list.isEmpty()) {
list.add(num);
} else {
Integer integer = list.get(0);
if (num > integer) {
list.add(0, num);
}
}
}
}
用协程循环1万次,日志:
start:9 platform threads
max:14 platform threads
cost:1210ms
1秒多实际只用了5个线程就结束了。
改成2.平台线程,普通500固定线程池1万次,日志:
start:9 platform threads
max:509 platform threads
cost:10541ms
10秒多用了500个线程才结束。
另参考GO协程(GO协程只需方法前加go即可)循环1万次方法,每次方法内部休眠500毫秒的调用:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var waitGroup sync.WaitGroup
start := time.Now()
size := 10000
waitGroup.Add(size)
for i := 0; i < size; i++ {
go func() {
defer waitGroup.Done()
time.Sleep(500 * time.Millisecond)
}()
}
waitGroup.Wait()
fmt.Println("cost:", time.Now().Sub(start).Milliseconds())
}
运行:go run 122401.go
cost: 558
500多毫秒1万次循环
通过上面的简单对比,可以看出用协程性能明显更优,由于对GO还不够熟,GO协程如有问题欢迎沟通。
Java21前,如用协程可使用非官网第三方库实现,比如quasar:co.paralleluniverse.fibers.Fiber 详情参考其他资料。如要升级Java到Java21,涉及太多中间件的兼容性问题,所以要升级还是得慎重分析。