虚拟线程

线程术语定义

操作系统线程(OS Thread):由操作系统管理,是操作系统调度的基本单位。

平台线程(Platform Thread):Java.Lang.Thread 类的每个实例,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射。

虚拟线程(Virtual Thread):一种轻量级,由 JVM 管理的线程。对应的实例 java.lang.VirtualThread 这个类。

载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程。

虚拟线程定义

JDK 中 java.lang.Thread 的每个实例都是一个平台线程。平台线程在底层操作系统线程上运行 Java 代码,并在代码的整个生命周期内独占操作系统线程,平台线程实例本质是由系统内核的线程调度程序进行调度,并且平台线程的数量受限于操作系统线程的数量

而虚拟线程(Virtual Thread)它不与特定的操作系统线程相绑定。它在平台线程上运行 Java 代码,但在代码的整个生命周期内不独占平台线程。这意味着许多虚拟线程可以在同一个平台线程上运行他们的 Java 代码,共享同一个平台线程。同时虚拟线程的成本很低,虚拟线程的数量可以比平台线程的数量大得多。

17.png

虚拟线程和平台线程的区别

首先,虚拟线程总是守护线程setDaemon (false)方法不能将虚拟线程更改为非守护线程。所以,需要注意的是,当所有启动的非守护线程都终止时,JVM将终止。这意味着JVM不会等待虚拟线程完成后才退出。

其次,即使使用setPriority()方法,虚拟线程始终具有normal的优先级,且不能更改优先级。在虚拟线程上调用此方法没有效果。

还有就是,虚拟线程是不支持stop()、suspend()或resume()等方法。这些方法在虚拟线程上调用时会抛出UnsupportedOperationException异常。

虚拟线程创建

  1. 直接创建
1
2
3
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("hello wolrd virtual thread");
});
  1. 创建虚拟线程但不自动运行,手动调用start()开始运行
1
2
3
4
Thread.ofVirtual().unstarted(() -> {
System.out.println("hello wolrd virtual thread");
});
vt.start();
  1. 通过虚拟线程的 ThreadFactory 创建虚拟线程
1
2
3
4
5
ThreadFactory tf = Thread.ofVirtual().factory();
Thread vt = tf.newThread(() -> {
System.out.println("Start virtual thread...");
});
vt.start();
  1. 通过Executors创建虚拟线程池
1
2
3
4
5
6
7
8
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> submit = executor.submit(() -> {
System.out.println("Start virtual thread...");
return "hello";
});
executor.execute(() -> {
System.out.println("Start virtual thread...");
});

虚拟线程实现原理

虚拟线程是由 Java 虚拟机调度,而不是操作系统。虚拟线程占用空间小,同时使用轻量级的任务队列来调度虚拟线程,避免了线程间基于内核的上下文切换开销,因此可以极大量地创建和使用。

简单来看,虚拟线程实现如下:virtual thread =continuation+scheduler+runnable

1
2
3
4
5
6
7
// 伪代码
mount();
try {
Continuation.run();
} finally {
unmount();
}

虚拟线程会把任务(java.lang.Runnable实例)包装到一个 Continuation 实例中:

  • 当任务需要阻塞挂起的时候,会调用 Continuation 的 yield 操作进行阻塞,虚拟线程会从平台线程卸载。

  • 当任务解除阻塞继续执行的时候,调用 Continuation.run 会从阻塞点继续执行。

    Scheduler 也就是执行器,由它将任务提交到具体的载体线程池中执行。

  • 它是 java.util.concurrent.Executor 的子类。

  • 虚拟线程框架提供了一个默认的 FIFO 的 ForkJoinPool 用于执行虚拟线程任务。

Runnable 则是真正的任务包装器,由 Scheduler 负责提交到载体线程池中执行。

JVM 把虚拟线程分配给平台线程的操作称为 mount(挂载),取消分配平台线程的操作称为 unmount(卸载):

mount 操作:虚拟线程挂载到平台线程,虚拟线程中包装的 Continuation 堆栈帧数据会被拷贝到平台线程的线程栈,这是一个从堆复制到栈的过程。

unmount 操作:虚拟线程从平台线程卸载,此时虚拟线程的任务还没有执行完成,所以虚拟线程中包装的 Continuation 栈数据帧会会留在堆内存中。

从 Java 代码的角度来看,其实是看不到虚拟线程及载体线程共享操作系统线程的,会认为虚拟线程及其载体都在同一个线程上运行,因此,在同一虚拟线程上多次调用的代码可能会在每次调用时挂载的载体线程都不一样。JDK 中使用了 FIFO 模式的 ForkJoinPool 作为虚拟线程的调度器,从这个调度器看虚拟线程任务的执行流程大致如下:

  1. 调度器(线程池)中的平台线程等待处理任务。
  2. 一个虚拟线程被分配平台线程,该平台线程作为载体线程执行虚拟线程中的任务。
  3. 虚拟线程运行其 Continuation,Mount(挂载)平台线程后,最终执行 Runnable 包装的用户实际任务。
  4. 虚拟线程任务执行完成,标记 Continuation 终结,标记虚拟线程为终结状态,清空上下文,等待 GC 回收,解除挂载载体线程会返还到调度器(线程池)中等待处理下一个任务。

image-20231207104910149

上面是没有阻塞场景的虚拟线程任务执行情况,如果遇到了阻塞(例如 Lock 等)场景,会触发 Continuation 的 yield 操作让出控制权,等待虚拟线程重新分配载体线程并且执行,具体见下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ReentrantLock lock = new ReentrantLock();
Thread.startVirtualThread(() -> {
lock.lock();
});
// 确保锁已经被上面的虚拟线程持有
Thread.sleep(1000);
Thread.startVirtualThread(() -> {
System.out.println("first");
//会触发Continuation的yield操作
lock.lock();
try {
System.out.println("second");
} finally {
lock.unlock();
}
System.out.println("third");
});
Thread.sleep(Long.MAX_VALUE);
}

性能差异

我们写一个简单的任务,在控制台中打印消息之前等待1秒:

1
2
3
4
5
6
7
8
9
10
final AtomicInteger atomicInteger = new AtomicInteger();

Runnable runnable = () -> {
try {
Thread.sleep(Duration.ofSeconds(1));
} catch(Exception e) {
System.out.println(e);
}
System.out.println("Work Done - " + atomicInteger.incrementAndGet());
};

现在,我们将从这个Runnable创建10000个线程,并使用虚拟线程和平台线程执行它们,以比较两者的性能。

先来我们比较熟悉的平台线程的实现:

1
2
3
4
5
6
7
8
9
10
11
Instant start = Instant.now();

try (var executor = Executors.newFixedThreadPool(100)) {
for(int i = 0; i < 10000; i++) {
executor.submit(runnable);
}
}

Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();
System.out.println("总耗时 : " + timeElapsed);

输出结果为:

总耗时 : 102323

总耗时大概100秒左右。接下来再用虚拟线程跑一下看看

在JDK 21中已经是正式功能了,但是在JDK 19中,虚拟线程是一个预览API,默认是禁用。所以需要使用$ java——source 19——enable-preview xx.java 的方式来运行代码。

1
2
3
4
5
6
7
8
9
10
11
Instant start = Instant.now();

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for(int i = 0; i < 10000; i++) {
executor.submit(runnable);
}
}

Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();
System.out.println("总耗时 : " + timeElapsed);

使用 Executors.newVirtualThreadPerTaskExecutor()来创建虚拟线程,执行结果如下:

总耗时 : 1674

总耗时大概1.6秒左右。

100秒和1.6秒的差距,足以看出虚拟线程的性能提升还是立竿见影的。