Java协程编程之Loom项目尝鲜

2,569 阅读6分钟

前提 之前很长一段时间关注 JDK 协程库的开发进度,但是前一段时间比较忙很少去查看 OpenJDK 官网的内容。 Java 协程项目 Loom (因为项目还在开发阶段, OpenJDK 给出的官网 openjdk.java.net/projects/lo… 中只有少量 Loom 项目相关的信息)已经在 2018 年之前立项,目前已经发布过基于 JDK17 编译和 JDK18 编译等早期版本,笔者在下载 Loom 早期版本的时候只找到 JDK18 编译的版本: 下载入口在: jdk.java.net/loom

由于该 JDK 版本过高,目前可以使用主流 IDE 导入 Loom-JDK-18+9 进行代码高亮和语法提醒,暂时找不到方法进行编译,暂时使用该 JDK 执行目录下的的 javac 命令脚本进行编译,使用 java 命令脚本运行。

Loom项目简单介绍

Loom - Fibers, Continuations and Tail-Calls for the JVM

Loom 项目的标题已经凸显了引入的三大新特性:

  • Fibers :几年前看过当时的 Loom 项目的测试代码就是使用 Fiber 这个 API (现在这个 API 已经被移除),意为轻量级线程,即协程,又称为轻量级用户线程,很神奇的是在目前的 JDK 中实际上称为 Virtual Thread ( 虚拟线程 )
  • Continuations :直译为"连续",实现上有点像闭包,参考不少资料,尚未准确理解其具体含义,感觉可以"粗暴"解读为"程序接下来要执行什么"或者"下一个要执行的代码块"
  • Tail-Calls :尾调用 VM 级别支持 三个新特性不详细展开,目前只是 EA 版本,还存在修改的可能性,所以也没必要详细展开。

Virtual Thread使用

当前版本 Loom 项目中协程使用并没有引入一个新的公开的虚拟线程 VirtualThread 类,虽然真的存在 VirtualThread ,但这个类使用 default 修饰符,隐藏在 java.lang 包中,并且 VirtualThreadThread 的子类。协程的创建 API 位于 Thread 类中:

当前版本 Loom 项目中协程使用并没有引入一个新的公开的虚拟线程 VirtualThread 类,虽然真的存在 VirtualThread ,但这个类使用 default 修饰符,隐藏在 java.lang 包中,并且 VirtualThread 是 Thread 的子类。协程的创建 API 位于 Thread 类中:

使用此 API 创建协程如下:

public static void main(String[] args) {
    Thread fiber = Thread.startVirtualThread(() -> System.out.println("Hello Fiber"));
}

从当前的源码可知:

VirtualThread 会通过 Thread.currentThread() 获取父线程的调度器,如果在 main 方法运行,那么上面代码中的协程实例的父线程就是 main 线程 默认的调度器为系统创建的 ForkJoinPool 实例( VirtualThread.DEFAULT_SCHEDULER ),输入的 Runnable 实例会被封装为RunContinuation,最终由调度器执行 对于 timed unpark (正在阻塞,等待唤醒)的协程,使用系统创建的ScheduledExecutorService实例进行唤醒 这个静态工厂方法创建完协程马上运行,返回的是协程实例 如果按照上面的 Thread.startVirtualThread() 方法去创建协程,显然无法定义协程的名称等属性。 Loom 项目为 Thread 类引入了建造者模式,比较合理地解决了这个问题:

// 创建平台线程建造器,对应于Thread实例
public static Builder.OfPlatform ofPlatform() {
    return new ThreadBuilders.PlatformThreadBuilder();
}

// 创建虚拟线程建造器,对应于VirtualThread
public static Builder.OfVirtual ofVirtual() {
    return new ThreadBuilders.VirtualThreadBuilder();
}

简单说就是:

ofPlatform() 方法用于构建 Thread 实例,这里的 Platform Thread (平台线程)其实就是 JDK1.0 引入的线程实例,普通的用户线程 ofVirtual() 方法用于构建 VirtualThread 实例,也就是构建协程实例 这两个建造器实例的所有 Setter 方法链展开如下:

public static void main(String[] args) {
    Thread.Builder.OfPlatform platformThreadBuilder = Thread.ofPlatform()
            // 是否守护线程
            .daemon(true)
            // 线程组
            .group(Thread.currentThread().getThreadGroup())
            // 线程名称
            .name("thread-1")
            // 线程名称前缀 + 起始自增数字 => prefix + start,下一个创建的线程名称就是prefix + (start + 1)
            // start > 0的情况下会覆盖name属性配置
            .name("thread-", 1L)
            // 是否启用ThreadLocal
            .allowSetThreadLocals(false)
            // 是否启用InheritableThreadLocal
            .inheritInheritableThreadLocals(false)
            // 设置优先级
            .priority(100)
            // 设置线程栈深度
            .stackSize(10)
            // 设置未捕获异常处理器
            .uncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {

                }
            });
    // thread-1
    Thread firstThread = platformThreadBuilder.unstarted(() -> System.out.println("Hello Platform Thread First"));
    // thread-2
    Thread secondThread = platformThreadBuilder.unstarted(() -> System.out.println("Hello Platform Thread Second"));
    Thread.Builder.OfVirtual virtualThreadBuilder = Thread.ofVirtual()
            // 协程名称
            .name("fiber-1")
            // 协程名称前缀 + 起始自增数字 => prefix + start,下一个创建的协程名称就是prefix + (start + 1)
            // start > 0的情况下会覆盖name属性配置
            .name("fiber-", 1L)
            // 是否启用ThreadLocal
            .allowSetThreadLocals(false)
            // 是否启用InheritableThreadLocal
            .inheritInheritableThreadLocals(false)
            // 设置调度器,Executor实例,也就是调度器是一个线程池,设置为NULL会使用VirtualThread.DEFAULT_SCHEDULER
            .scheduler(null)
            // 设置未捕获异常处理器
            .uncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {

                }
            });
    // fiber-1
    Thread firstFiber = virtualThreadBuilder.unstarted(() -> System.out.println("Hello Platform Virtual First"));
    // fiber-2
    Thread secondFiber = virtualThreadBuilder.unstarted(() -> System.out.println("Hello Platform Virtual Second"));
}

这里可以发现一点,就是 建造器是可以复用的 。如果想用建造器创建同一批参数设置相同的线程或者协程,可以设置 name(String prefix, long start) 方法,定义线程或者协程的名称前缀和一个大于等于 0 的数字,反复调用 Builder#unstarted(Runnable task) 方法就能批量创建线程或者协程,名称就设置为 prefix + start prefix + (start + 1)prefix + (start + 2) 以此类推。协程创建基本就是这么简单,运行的话直接调用 start() 方法:

public class FiberSample2 {

    public static void main(String[] args) throws Exception {
        Thread.ofVirtual()
                .name("fiber-1")
                .allowSetThreadLocals(false)
                .inheritInheritableThreadLocals(false)
                .unstarted(() -> {
                    Thread fiber = Thread.currentThread();
                    System.out.printf("[%s,daemon:%s,virtual:%s] - Hello World\n", fiber.getName(),
                            fiber.isDaemon(), fiber.isVirtual());
                }).start();
        // 主线程休眠
        Thread.sleep(Long.MAX_VALUE);
    }
}

目前无法在主流 IDE 编译上面的类,所以只能使用该 JDK 目录下的工具编译和运行,具体如下:

# 执行 - 当前目录I:\J-Projects\framework-source-code\fiber-sample\src\main\java
(1)编译:I:\Environment\Java\jdk-18-loom\bin\javac.exe I:\J-Projects\framework-source-code\fiber-sample\src\main\java\cn\throwx\fiber\sample\FiberSample2.java
(2)执行main方法:I:\Environment\Java\jdk-18-loom\bin\java.exe  cn.throwx.fiber.sample.FiberSample2

这里也看出了一点,所有的协程实例的 daemon 标识默认为 true 且不能修改。 ##小结 如果用尝鲜的角度去使用 Loom 项目,可以提前窥探 JVM 开发者们是如何基于协程这个重大特性进行开发的,这对于提高学习 JDK 内核代码的兴趣有不少帮助。从目前来看,对于协程的实现 Loom 项目距离 RELEASE 版本估计还有不少功能需要完善,包括新增 API 的稳定性,以及协程是否能够移植到原有的 JUC 类库中使用(当前的 Loom-JDK-18+9 没有对原来的线程池等类库进行修改)等问题需要解决,所以在保持关注的过程中静心等待吧。