当前位置: 首页 > news >正文

百日筑基第五十七天-虚拟线程

百日筑基第五十七天-虚拟线程

前提

JDK192022-09-20发布GA版本,该版本提供了虚拟线程的预览功能。下载JDK19之后翻看了一下有关虚拟线程的一些源码,跟早些时候的Loom项目构建版本基本并没有很大出入,也跟第三方JDK如鹅厂的Kona虚拟线程实现方式基本一致,这里分析一下虚拟线程设计与源码实现。

Platform Thread与Virtual Thread

因为引入了虚拟线程,原来JDK存在java.lang.Thread类,俗称线程,为了更好地区分虚拟线程和原有的线程类,引入了一个全新类java.lang.VirtualThreadThread类的一个子类型),直译过来就是"虚拟线程"。

Thread在此基础上做了不少兼容性工作。此外,还应用了建造者模式引入了线程建造器,提供了静态工厂方法Thread.ofPlatform()Thread.ofVirtual()分别用于实例化Thread(工厂)建造器和VirtualThread(工厂)建造器,顾名思义,两种建造器分别用于创建Thread或者VirtualThread,例如:

// demo-1 build platform thread
Thread platformThread = Thread.ofPlatform().daemon().name("worker").unstarted(runnable);// demo-2 create platform thread factory
ThreadFactory platformThreadFactory = Thread.ofPlatform().daemon().name("worker-", 0).factory();// demo-3 build virtual thread
Thread virtualThread = Thread.ofVirtual().name("virtual-worker").unstarted(runnable);// demo-4 create virtual thread factory
ThreadFactory virtualThreadFactory = Thread.ofVirtual().name("virtual-worker-", 0).factory();

更新的JDK文档中也把原来的Thread称为Platform Thread,可以更明晰地与Virtual Thread区分开来。这里Platform Thread直译为"平台线程",其实就是"虚拟线程"出现之前的老生常谈的"线程"。

那么平台线程与虚拟线程的联系和区别是什么?JDK中的每个java.lang.Thread实例也就是每个平台线程实例都在底层操作系统线程上运行Java代码,并且平台线程在运行代码的整个生命周期内捕获系统线程。可以得出一个结论,平台线程与底层系统线程是一一对应的,平台线程实例本质是由系统内核的线程调度程序进行调度,并且平台线程的总数量受限于系统线程的总数量。

在这里插入图片描述

总的来说,平台线程有下面的一些特点或者说限制:

  • 资源有限导致系统线程总量有限,进而导致与系统线程一一对应的平台线程有限
  • 平台线程的调度依赖于系统的线程调度程序,当平台线程创建过多,会消耗大量资源用于处理线程上下文切换
  • 每个平台线程都会开辟一块私有的栈空间,大量平台线程会占据大量内存

这些限制导致开发者不能极大量地创建平台线程,为了满足性能需要,需要引入池化技术、添加任务队列构建消费者-生产者模式等方案去让平台线程适配多变的现实场景。显然,开发者们迫切需要一种轻量级线程实现,刚好可以弥补上面提到的平台线程的限制,这种轻量级线程可以满足:

  • 可以大量创建,例如十万级别、百万级别,而不会占据大量内存
  • JVM进行调度和状态切换,并且与系统线程"松绑"
  • 用法与原来平台线程差不多,或者说尽量兼容平台线程现存的API

虚拟线程就是为了解决这个问题,看起来它的运行示意图如下:

在这里插入图片描述

(当然,平台线程不是简单地与虚拟线程进行1:N的绑定)

虚拟线程实现原理

虚拟线程是一种轻量级(用户模式)线程,这种线程是由Java虚拟机调度,而不是操作系统。虚拟线程占用空间小,任务切换开销几乎可以忽略不计,因此可以极大量地创建和使用。总体来看,虚拟线程实现如下:

virtual thread = continuation + scheduler

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

  • 当任务需要阻塞挂起的时候,会调用Continuationyield操作进行阻塞
  • 当任务需要解除阻塞继续执行的时候,Continuation会被继续执行

Scheduler也就是执行器,会把任务提交到一个载体线程池中执行:

  • 执行器是java.util.concurrent.Executor的子类
  • 虚拟线程框架提供了一个默认的ForkJoinPool用于执行虚拟线程任务

下文会把carrier thread称为"载体线程",指的是负责执行虚拟线程中任务的平台线程,或者说运行虚拟线程的平台线程称为它的载体线程

操作系统调度系统线程,而Java平台线程与系统线程一一映射,所以平台线程被操作系统调度,但是虚拟线程是由JVM调度。JVM把虚拟线程分配给平台线程的操作称为mount(挂载),反过来取消分配平台线程的操作称为unmount(卸载):

  • mount操作:虚拟线程挂载到平台线程,虚拟线程中包装的Continuation栈数据帧或者引用栈数据会被拷贝到平台线程的线程栈,这是一个从堆复制到栈的过程
  • unmount操作:虚拟线程从平台线程卸载,大多数虚拟线程中包装的Continuation栈数据帧会留在堆内存中

这个mount -> run -> unmount过程用伪代码表示如下:

mount();
try {Continuation.run();
} finally {unmount();
}

Java代码的角度来看,虚拟线程和它的载体线程暂时共享一个OS线程实例这个事实是不可见,因为虚拟线程的堆栈跟踪和线程本地变量与平台线程是完全隔离的。JDK中专门是用了一个FIFO模式的ForkJoinPool作为虚拟线程的调度程序,从这个调度程序看虚拟线程任务的执行流程大致如下:

  • 调度器(线程池)中的平台线程等待处理任务

在这里插入图片描述

  • 一个虚拟线程被分配平台线程,该平台线程作为运载线程执行虚拟线程中的任务

在这里插入图片描述

  • 虚拟线程运行其Continuation,从而执行基于Runnable包装的用户任务

在这里插入图片描述

  • 虚拟线程任务执行完成,标记Continuation终结,标记虚拟线程为终结状态,清空一些上下文变量,运载线程"返还"到调度器(线程池)中作为平台线程等待处理下一个任务

在这里插入图片描述

上面是描述一般的虚拟线程任务执行情况,在执行任务时候首次调用Continuation#run()获取锁(ReentrantLock)的时候会触发Continuationyield操作让出控制权,等待虚拟线程重新分配运载线程并且执行,见下面的代码:

public class VirtualThreadLock {public static void main(String[] args) throws Exception {ReentrantLock lock = new ReentrantLock();Thread.startVirtualThread(() -> {lock.lock();     // <------ 这里确保锁已经被另一个虚拟线程持有});Thread.sleep(1000);Thread.startVirtualThread(() -> {System.out.println("first");lock.lock();try {System.out.println("second");} finally {lock.unlock();}System.out.println("third");});Thread.sleep(Long.MAX_VALUE);}
}
  • 虚拟线程中任务执行时候首次调用Continuation#run()执行了部分任务代码,然后尝试获取锁,会导致Continuationyield操作让出控制权(任务切换),也就是unmount,运载线程栈数据会移动到Continuation栈的数据帧中,保存在堆内存,虚拟线程任务完成(但是虚拟线程没有终结,同时其Continuation也没有终结和释放),运载线程被释放到执行器中等待新的任务;如果Continuationyield操作失败,则会对运载线程进行park调用,阻塞在运载线程上

在这里插入图片描述

  • 当锁持有者释放锁之后,会唤醒虚拟线程获取锁(成功后),虚拟线程会重新进行mount,让虚拟线程任务再次执行,有可能是分配到另一个运载线程中执行,Continuation栈会的数据帧会被恢复到运载线程栈中,然后再次调用Continuation#run()恢复任务执行:

在这里插入图片描述

  • 最终虚拟线程任务执行完成,标记Continuation终结,标记虚拟线程为终结状态,清空一些上下文变量,运载线程"返还"到调度器(线程池)中作为平台线程等待处理下一个任务

Continuation组件十分重要,它既是用户真实任务的包装器,也是任务切换虚拟线程与平台线程之间数据转移的一个句柄,它提供的yield操作可以实现任务上下文的中断和恢复。由于Continuation被封闭在java.base/jdk.internal.vm下,可以通过增加编译参数--add-exports java.base/jdk.internal.vm=ALL-UNNAMED暴露对应的功能,从而编写实验性案例,IDEA中可以按下图进行编译参数添加:

在这里插入图片描述

然后编写和运行下面的例子:

import jdk.internal.vm.Continuation;
import jdk.internal.vm.ContinuationScope;public class ContinuationDemo {public static void main(String[] args) {ContinuationScope scope = new ContinuationScope("scope");Continuation continuation = new Continuation(scope, () -> {System.out.println("Running before yield");Continuation.yield(scope);System.out.println("Running after yield");});System.out.println("First run");// 第一次执行Continuation.runcontinuation.run();System.out.println("Second run");// 第二次执行Continuation.runcontinuation.run();System.out.println("Done");}
}// 运行代码,神奇的结果出现了
First run
Running before yield
Second run
Running after yield
Done

这里可以看出Continuation的奇妙之处,Continuation实例进行yield调用后,再次调用其run方法就可以从yield的调用之处往下执行,从而实现了程序的中断和恢复。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 前端框架(三件套)
  • git cherry-pick命令使用分享
  • Android UI:PopupWindow:API
  • 《机器学习》 逻辑回归 大批量数据的下采样 <8>
  • git本地仓库同步到远程仓库
  • 黑客入门教程(非常详细)从零基础入门到精通,看完这一篇就够了
  • EasyExcel_通过模板导出(多sheet、列表、图片)
  • Linux ps命令详解
  • 【NI国产替代】NI‑9235四分之一桥应变计,8通道C系列应变/桥输入模块
  • 基于LSTM的交通流量预测算法及Python实现
  • ECMAScript性能优化技巧与陷阱(上)
  • 上门搬家小程序源码开发:打造便捷高效的搬家新体验
  • Triplet Loss解析及示例计算
  • 私域流量与公域流量的主要区别
  • 【游戏】什么是摄影游戏(Photography Games)
  • 3.7、@ResponseBody 和 @RestController
  • Akka系列(七):Actor持久化之Akka persistence
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • Git学习与使用心得(1)—— 初始化
  • pdf文件如何在线转换为jpg图片
  • php中curl和soap方式请求服务超时问题
  • Python十分钟制作属于你自己的个性logo
  • 从零开始学习部署
  • 微信支付JSAPI,实测!终极方案
  • 想写好前端,先练好内功
  • 掌握面试——弹出框的实现(一道题中包含布局/js设计模式)
  • FaaS 的简单实践
  • ​1:1公有云能力整体输出,腾讯云“七剑”下云端
  • ​secrets --- 生成管理密码的安全随机数​
  • #微信小程序:微信小程序常见的配置传值
  • $refs 、$nextTic、动态组件、name的使用
  • (175)FPGA门控时钟技术
  • (C语言)共用体union的用法举例
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (zt)最盛行的警世狂言(爆笑)
  • (亲测有效)推荐2024最新的免费漫画软件app,无广告,聚合全网资源!
  • (一)基于IDEA的JAVA基础10
  • . NET自动找可写目录
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .equals()到底是什么意思?
  • .gitignore文件使用
  • .Net CF下精确的计时器
  • .net CHARTING图表控件下载地址
  • .NET Core 发展历程和版本迭代
  • /usr/local/nginx/logs/nginx.pid failed (2: No such file or directory)
  • @WebServiceClient注解,wsdlLocation 可配置
  • [ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解
  • [ IO.File ] FileSystemWatcher
  • [Android]RecyclerView添加HeaderView出现宽度问题
  • [BeginCTF]真龙之力
  • [Bugku]密码???[writeup]
  • [C puzzle book] types
  • [c++] 什么是平凡类型,标准布局类型,POD类型,聚合体
  • [ccc3.0][数字钥匙] UWB配置和使用(二)
  • [CISCN2019 华东南赛区]Web111