目录
一、Java的线程模型
二、Quasar From parallel universe
三、Project loom
本文链接:Java的虚拟线程和结构化并发,含完整示例代码
Java语言对于线程进行了完整抽象,你无需关注各类操作系统的差异,写出并发程序不需要特别陡峭的学习曲线,想想就特别美好。
但现实是这一统一的线程模型在云原生时代反而失去了优势,因为与操作系统1:1对应的线程模型,导致Java的线程是一种非常重量级的线程(内存占用和上下文切换)。
64 位 Linux 上 HotSpot 的线程栈容量默认是1MB,线程的内核元数据(Kernel Metadata)还要额外消耗 2-16KB 内存,所以单个虚拟机的最大线程数量一般只会设置到 200 至 400 条,当程序员把数以百万计的请求往线程池里面灌时,系统即便能处理得过来,其中的切换损耗也相当可观的。
这种模型对于CPU密集型的应用会有更好的资源利用率,但对于IO密集型的就会十分痛苦。为提高Java中IO的性能,我们大量使用Reactor编程模型、Netty的非阻塞多路复用IO几乎成为了规范,但这类IO场景在底层实现上有两大挑战:
1.大量线程,IO操作越多,为提高吞吐量往往增加更多线程。
2.异步编程,为避免线程阻塞,必须采用异步编程,但很明显,异步可读性差、嵌套过深、反人类。
如何解决这个问题呢?
核心是要重新设计线程模型和结构化并发。
Green Thread是上古Java采用过的模型,OS Thread是当前Java使用过的,Virtual Thread是正在努力进行的。
结构化并发是 2016 年才提出的新的并发编程概念。可以让你像写同步代码的方式写异步代码。
从两个项目开始深入介绍一下Java在线程模型上的努力和发展趋势。
Quasar 是一个为 Java 和Kotlin提供高性能轻量级线程、类似 Go 的channels、类似 Erlang 的 actors 以及其他异步编程工具的库。
Quasar 由Parallel Universe开发并作为自由软件发布,根据 Eclipse 公共许可证和GNU Lesser General Public License 获得双重许可。
不过这个框架在2018年已停止维护。目前已迁移到即将介绍的Project Loom中。
虽然已经废弃,但在2018年属于Java非常前沿的技术,当时也做了具体的调研和开发验证,最大的作用是轻量级线程Fiber的运用和像写同步代码的方式写异步代码的编程模式(不过实现这种效果还是比较复杂)。
以下是当时改造Tomcat以支持servlet逻辑中fiber线程的运用:
maven配置
co.paralleluniverse
quasar-core
0.7.10
扩展Tomcat的业务执行线程池:
/**
* 测试 使用Servlet的方式,而非替换内部线程执行
* 2018/8/28.
*/
public class FiberTomcatThreaPool extends StandardThreadExecutor {
public FiberTomcatThreaPool(){
super();
System.out.println("[Fiber] init..."+Serializer.class);
}
@Override
public void execute(Runnable command, long timeout, TimeUnit unit) {
System.out.println("[Fiber] begin..."+Serializer.class);
System.out.println("[Fiber] begin..."+Thread.currentThread().getContextClassLoader());
new Fiber("task", new SuspendableRunnable() {
@Override
public void run() throws SuspendExecution, InterruptedException {
command.run();
}
}).start();
}
@Override
public void execute(Runnable command) {
System.out.println("[Fiber] begin..."+Serializer.class);
System.out.println("[Fiber] begin..."+Thread.currentThread().getContextClassLoader());
new Fiber("task", new SuspendableRunnable() {
@Override
public void run() throws SuspendExecution, InterruptedException {
System.out.println("[Fiber] run...");
command.run();
}
}).start();
}
}
可以看到这种使用轻量级线程的方式并没有像Go语言那样从语言级做到支持,写起来依然显得别扭。这也是后起之秀Project loom的努力方向。
Loom 项目的目标是让 Java 支持额外的N:M 线程模型 ,请注意是“额外支持”,而不是像当年从绿色线程过渡到内核线程那样的直接替换,也不是像 Solaris 平台的 HotSpot 虚拟机那样通过参数让用户二选其一。Loom 项目新增加一种“虚拟线程”(Virtual Thread,以前也叫Fiber ),本质上它是一种有栈协程(Stackful Coroutine),多条虚拟线程可以映射到同一条物理线程之中,在用户空间中自行调度,每条虚拟线程的栈容量也可由用户自行决定。
Loom 项目的另一个目标是要尽最大可能保持原有统一线程模型的交互方式,通俗地说就是原有的 Thread、J.U.C、NIO、Executor、Future、ForkJoinPool 等这些多线程工具都应该能以同样的方式支持新的虚拟线程,原来多线程中你理解的概念、编码习惯大多数都能够继续沿用。
Loom 的另一个重点改进是支持结构化并发 (Structured Concurrency),就是上文提到的异步改同步的编程方式。
https://www.baeldung.com/openjdk-project-loom
如果运行Loom功能,请参考本人另外一篇文章:IntelliJ IDEA运行JDK 19-ea问题
public static void testVT() throws InterruptedException {
System.out.println("Current jdk version:"+System.getProperty("java.version") );
System.out.println("1-Run in main thread, Currrent ThreadName:"+Thread.currentThread().getName());
Thread thread1 = Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println("Run in virtual thread, Currrent ThreadName:"+Thread.currentThread());
}
});
System.out.println("2-Run in main thread, Currrent ThreadName:"+Thread.currentThread().getName());
thread1.join();
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Run in classic thread, Currrent ThreadName:"+Thread.currentThread().getName());
}
});
thread.start();
thread.join();
}
上述代码运行输出如下:
Current jdk version:19-ea
1-Run in main thread, Currrent ThreadName:main
2-Run in main thread, Currrent ThreadName:main
Run in virtual thread, Currrent ThreadName:VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
Run in classic thread, Currrent ThreadName:Thread-0
可以看到Virtual Thread是在ForkJoinPool的一个实际线程中运行。
使用方式相对上文的Quasar已经有很大的改善,并且是在我们常用的Thread抽象之下,除了实例化,其他API大部分保持一致。
private static void testQ() throws InterruptedException {
var queue = new SynchronousQueue();
Thread.ofVirtual().start(() -> {
try {
System.out.println("Run in v thread :"+Thread.currentThread());
Thread.sleep(Duration.ofSeconds(2));
queue.put("done");
System.out.println("finish v thread :"+Thread.currentThread());
} catch (InterruptedException e) { }
});
System.out.println("Run in main thread :"+Thread.currentThread());
String msg = queue.take();
System.out.println("finish main thread :"+Thread.currentThread());
}
输出:
Run in main thread :Thread[#1,main,5,main]
Run in v thread :VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
finish v thread :VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
finish main thread :Thread[#1,main,5,main]
根据日志打印顺序,可以看出行为跟普通线程完全一致。
再来看下一个Loom的重点特性:结构化并发。
假设有如下传统单线程的任务:
//父任务
Response handle() throws IOException {
String theUser = findUser();
int theOrder = fetchOrder();
return new Response(theUser, theOrder);
}
//子任务1
private static String findUser() throws InterruptedException {
System.out.println("begin-Run in findUser :"+Thread.currentThread());
Thread.sleep(Duration.ofSeconds(2));
System.out.println("end-Run in findUser :"+Thread.currentThread());
return "return of findUser";
}
//子任务2
private static String fetchOrder() throws InterruptedException {
System.out.println("begin-Run in fetchOrder :"+Thread.currentThread());
Thread.sleep(Duration.ofSeconds(2));
System.out.println("end-Run in fetchOrder :"+Thread.currentThread());
return "return of fetchOrder";
}
static class Response{
String p1;
String p2;
Response(String p1,String p2){
this.p1 = p1;
this.p2 = p2;
}
@Override
public String toString() {
return "Response{" +
"p1='" + p1 + '\\'' +
", p2='" + p2 + '\\'' +
'}';
}
}
代码结构和运行结构完全一致,易理解、易调试,这是典型的面向过程程序,同时是结构化的,但随着在当前线程模型的约束下,为压榨计算机资源,实现高性能,采用多线程并发运行,被改造成如下的形式。
传统多线程执行使用如下代码:
Response handle() throws ExecutionException, InterruptedException {
Future user = executorService.submit(() -> findUser());
Future order = executorService.submit(() -> fetchOrder());
String theUser = user.get(); // 连接findUser
int theOrder = order.get(); // 连接fetchOrder
return new Response(theUser, theOrder);
}
handle定义为父任务,findUser和fetchOrder是子任务,通过Future的get方法阻塞等待结果。
1.阻塞线程造成线程资源浪费和上下文切换开销;
2.子任务独立运行,父任务失败无法终止子任务;
3.子任务具备独立线程堆栈,调试非常麻烦;
4.代码虽然是按顺序写的,但是执行过程却是意大利面式
的,非结构化的。
这就是所谓的非结构化的并发编程。
因此在2016 年,Martin Sústrik 在他的博文中创造了“结构化并发”这个术语, Nathaniel J. Smith 在他关于结构化并发的文章中推广了这个术语。
在Loom的努力下,提出了结构化并发的JEP 428项目,在不同线程中运行的多个任务视为原子操作,简化多线程编程。它可以简化错误处理和取消操作,提高可靠性,并增强可观察性。
使用 StructuredTaskScope 类来组织并发代码,这个类将把一组子任务视为一个单元。子任务通过单独的线程创建,然后连接成一个单元,也可以作为一个单元进行取消。子任务的异常或执行结果将由父任务进行聚合和处理。
实现方式如下:
private static Response handle() throws ExecutionException, InterruptedException {
System.out.println("1-Run in main thread :"+Thread.currentThread());
var scope = new StructuredTaskScope.ShutdownOnFailure();
System.out.println("2-Run in main thread :"+Thread.currentThread());
Future user = scope.fork(() -> findUser());
System.out.println("3-Run in main thread :"+Thread.currentThread());
Future order = scope.fork(() -> fetchOrder());
System.out.println("4-Run in main thread :"+Thread.currentThread());
scope.join(); // 连接
scope.throwIfFailed(); // 抛出错误
System.out.println("5-Run in main thread :"+Thread.currentThread());
// 聚合结果
return new Response(user.resultNow(), order.resultNow());
}
通过以下命令执行,注意参数--release 19 --enable-preview --add-modules jdk.incubator.concurrent
,一个都不能少。/Applications/jdk-19.jdk/Contents/Home/bin/是我JDK 19-ea
的安装目录。
/Applications/jdk-19.jdk/Contents/Home/bin/javac --release 19 --enable-preview --add-modules jdk.incubator.concurrent com/echx/jdk19/TestLoom.java
编译输出如下(成功!警告可忽略):
警告: 使用 incubating 模块: jdk.incubator.concurrent 注: com/echx/jdk19/TestLoom.java 使用 Java SE 19 的预览功能。 注: 有关详细信息,请使用 -Xlint:preview 重新编译。 1 个警告
开始运行
/Applications/jdk-19.jdk/Contents/Home/bin/java --enable-preview --add-modules jdk.incubator.concurrent com.echx.jdk19.TestLoom
运行结果如下:
1-Run in main thread :Thread[#1,main,5,main]
2-Run in main thread :Thread[#1,main,5,main]
3-Run in main thread :Thread[#1,main,5,main]
begin-Run in findUser :VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
4-Run in main thread :Thread[#1,main,5,main]
begin-Run in fetchOrder :VirtualThread[#23]/runnable@ForkJoinPool-1-worker-2
end-Run in findUser :VirtualThread[#21]/runnable@ForkJoinPool-1-worker-2
end-Run in fetchOrder :VirtualThread[#23]/runnable@ForkJoinPool-1-worker-1
5-Run in main thread :Thread[#1,main,5,main]
Response{p1='return of findUser', p2='return of fetchOrder'}