01 March 2023

JDK20已经发布,搭载了第二轮虚拟线程preview。目前的JEP 444 中,已经去掉了 preview 标识,有望在下一个LTS版本JDK21中发布,发布日期为2023-09. 最新的虚拟线程将支持ThreadLocal.

  1. 概述 jdk19 已经于2022-09-20 正式发布,下载地址:https://jdk.java.net/19/ ,包含以下特性:
  • 405: Record Patterns (Preview)
  • 422: Linux/RISC-V Port
  • 424: Foreign Function & Memory API (Preview)
  • 425: Virtual Threads (Preview)
  • 426: Vector API (Fourth Incubator)
  • 427: Pattern Matching for switch (Third Preview)
  • 428: Structured Concurrency (Incubator)

其中最令人激动的两个特性就是project loom项目带来的 JEP 425 虚拟线程(Virtual Threads) 和 JEP 428 结构化并发(Structured Concurrency).oracle 官方对于project loom非常看重,jdk 并发库作者、并发专家 brain goetz 也在采访中谈到,”project loom 将会杀死响应式编程”。

本文将着重介绍 虚拟线程(Virtual Thread).

  1. 虚拟线程

虚拟线程类似于协程,是一种用户态线程,由编程语言来管理与系统线程的映射关系。类似于go语言和kotlin的协程,但是比它们更强大和简洁。

实际上,java 曾经在非常古老的版本中就已经有用户态线程了,当时称为绿色线程,但它是M:1模型,即所有的绿色线程之对应一个平台线程。这种方式很快被全量使用平台线程的方式取代了。直到现在,project loom 带了新的调度器和新的工具类Continuation从而能支持M:N模型的用户态线程。

该图可以展示出平台线程(carrier thread) 和 虚拟线程(virtual thread)的关系,每当虚拟线程blocked或者waiting时,调度器会自动调度平台线程去承接其他虚拟线程的工作.

_config.yml

virutal thread 由 Continuation 和 Scheduler 组成:

Continuation:负责调整虚拟线程的状态,运行还是让出; yield方法可以让出对应的平台线程,run方法可以分配分配一个平台线程,继续运行 一般情况下,开发者不会直接接触该API,该api由虚拟线程封装。

该类定义如下:

package java.lang; 
public class Continuation implements Runnable {
public Continuation(ContinuationScope scope, Runnable target) 
public final void run() 
public static void yield(ContinuationScope scope) 
public boolean isDone() : }

scheduler 则由新的fork-join pool 承担,负责线程的调度工作;该pool并非fork-join poll的 common pool ,而是一个新的独立的pool 下面的例子会演示它们是两个独立的pool

创建虚拟线程 虚拟线程基于 Thread 类新增了一些方法,这些方法可以方便地创建平台线程或者虚拟线程。jdk 并没有提供一个新的线程类,所以这方面的兼容性非常好。

Thread.ofPlatfrom() // 生成平台线程,也就是我们现在用的线程
Thread.ofVirtual() // 生成虚拟线程

特性 thread 提供的方法已经在内部实现了对于平台线程和虚拟线程的兼容,比如下面的sleep,当检测到当前线程是虚拟线程时,最终sleep的逻辑的就是调用Continuation.yield()来实现sleep;

public static void sleep(long millis) throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (currentThread() instanceof VirtualThread vthread) {
        long nanos = MILLISECONDS.toNanos(millis);
        vthread.sleepNanos(nanos);
        return;
    }

    if (ThreadSleepEvent.isTurnedOn()) {
        ThreadSleepEvent event = new ThreadSleepEvent();
        try {
            event.time = MILLISECONDS.toNanos(millis);
            event.begin();
            sleep0(millis);
        } finally {
            event.commit();
        }
    } else {
        sleep0(millis);
    }
}

虚拟线程的阻塞代价非常低,你可以放心地去阻塞虚拟线程。当虚拟线程被阻塞时,会把当前虚拟线程所对应的stack 导出到堆中,等待阻塞结束,可以运行时,又从内存中讲其恢复,恢复时,可能会切换到另外一个虚拟线程。这个过程中,可以保证平台线程始终在做不阻塞的任务,从而提升平台线程的利用率;

这里将创建1百万个线程,每个线程休眠10s,使用平台线程和虚拟线程做对比:

1m thread sleep test

_config.yml _config.yml

使用平台线程时,很快就因为占用的内存过多而导致oom

更换为虚拟线程

_config.yml

成功运行了 一百万次

不同的fork-join pool

public class PoolName {
    public static void main(String[] args) {
        ForkJoinPool.commonPool().execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread());
            }
        });
        try {
            Thread.ofVirtual().start(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread());
                }
            }).join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
Thread[#21,ForkJoinPool.commonPool-worker-1,5,main]
VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1

使用虚拟线程,你可以放心大胆地使用上百万的线程完成工作,不需要担心它们使用过多的平台线程资源。下面的例子将会展示使用1千万的虚拟线程,最终只使用了7个平台线程。

public static void main(String[] args) {
    AtomicInteger ai  = new AtomicInteger();
    Set<String> set = new HashSet<>();
    for(int i = 0;i<10_000_000;i++){
        try {
            Thread.ofVirtual().start(new Runnable() {
                @Override
                public void run() {
                    try {
                        set.add(realWorkerName(Thread.currentThread().toString()));
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }).join();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    System.out.println(set.size());
    System.out.println(set);
}

private static String realWorkerName(String threadName){
    return threadName.substring(threadName.indexOf("@"),threadName.length());
}

运行结果

7
[@ForkJoinPool-1-worker-6, @ForkJoinPool-1-worker-7, @ForkJoinPool-1-worker-4, @ForkJoinPool-1-worker-5, @ForkJoinPool-1-worker-2, @ForkJoinPool-1-worker-3, @ForkJoinPool-1-worker-1]

在笔者的mbp m1 (8c 16g)上,只需要使用7个平台线程即可支撑1千万的虚拟线程,这就是虚拟线程的强大.

io 优化 jdk修改了所有bio阻塞通信的地方;运行在虚拟线程环境中,当代码阻塞时(和Thread.sleep()一样的原理,针对虚拟线程做了兼容),它会将当前的状态保存到内存中,当代码阻塞结束时,会自动唤醒,并重新运行(可能会切换到另外一个虚拟线程)

vthread 和 pthread 的关系

_config.yml

关于 虚拟线程,更多的信息可以到 JEP 了解: https://openjdk.org/jeps/425

  1. 总结 虚拟线程拥有以下特性: 很大程度地提升java程序的吞吐量(并不会变快) 完全兼容原有Thread类api,现有程序的变更成本较小 轻量级线程扩展,阻塞成本足够低,可以一次性运行上千万的虚拟线程 以同步方式编写异步代码(甚至不会出现满地的async、await,而C#和kotlin有),降低异步成的开发、调试成本

虚拟线程目前还是preview的状态,到正式发布可能还需要几个jdk版本迭代,期望未来的JDK21就能搭载正式版本的虚拟线程,这将彻底改变java的并发编程.