本文涉及到的技术:虚拟线程、结构化并发、线程池、TheadLocal,对原理感兴趣的可以直接跳到原理部分。
虚拟线程是JDK19中引入的,JDK21正式发布,我们先来看看虚拟线程的几种用法,然后再来分析底层实现原理。
先定义一个Runnable:
通过观察输出结果,就能知道当前运行Task
的是不是虚拟线程。
也可以增加以下代码直接判断是不是虚拟线程:
Thread.ofVirtual()
手动开启虚拟线程执行任务: 自动开启虚拟线程执行任务:
两者输出结果类似,为:
根据名字可以看出确实是用的VirtualThread ,但似乎跟ForkJoinPool有关,后面会分析。
我们也可以通过以下方式来创建普通线程:
输出结果为:
确实是普通线程。
还可以先得到一个ThreadFactory,然后来创建虚拟线程:
输出结果为:
还有一种更简单的API:
输出结果为:
结构化并发
在JDK21中还有一个新特性(预览版),叫做结构化并发,也会自动创建虚拟线程来运行代码,比如:
输出结果为:
Executors
还有一种和线程池类似的使用方式:
以上代码中每个任务运行时都会开启一个虚拟线程,输出�结果为:
表示有3个虚拟线程。
虚拟线程底层原理
以上大概就是使用或创建虚拟线程的几种情况了,那到底什么是虚拟线程呢?它跟线程有什么关系?它跟ForkJoinPool又有什么关系呢?
虚拟线程毕竟是虚拟 的,就像虚拟机也是虚拟的,是需要真实操作系统来支撑运行的。而虚拟线程仍然是基于线程来进行调度执行的。
我们先来看看普通线程的缺点在哪,看下面代码:
假如是一个普通线程执行上述代码,在输出完"before"后,线程就会睡眠1秒,然后才会输出"after",如果是一个线程要执行3个这样的任务,比如:
生成一个只有一个线程的线程池,用它来执行三个任务,实际上就是串行执行这三个任务,输出结果为:
但是,我们好好想想:当这个普通线程执行完第一个任务的"before"后,需要等1s 才执行"after",那能不能在等1s的过程中去执行第二个任务的"before"呢?原则上是可以的,这就是虚拟线程要优化的点。
大家好好理解一下上面的这句话,这是精髓
我们来看改成虚拟线程后的运行效果,先修改Task:
然后运行:
输出结果为:
大家运行时可能会发现有多个不同的ForkJoinPool-1-worker,那是因为我做了配置,后面会解释
不知道大家能不能看懂这个效果,我们可以发现有3个虚拟线程:VirtualThread[#21]
、VirtualThread[#23]
、VirtualThread[#24]
,但是只有一个线程:ForkJoinPool-1-worker-1
,虽然只有一个线程,却达到了并行执行三个任务的效果,其原理就是上面所分析的:
- 线程先执行任务1,任务1睡眠的过程中,线程会去执行任务2
- 任务2睡眠的过程中,线程会去执行任务3
- 任务3睡眠的过程中,线程暂时没有任务执行了
- 过一会,任务1睡眠结束,线程继续执行任务1
- 然后,任务2睡眠结束,线程继续执行任务2
- 最后,任务3睡眠结束,线程继续执行任务3
这样就达到了一个线程并行执行三个任务的效果,从中,我们可以看到,线程需要知道:一个任务什么时候开始睡眠了,什么时候睡眠结束了,哪个任务还没开始执行,哪个任务已经在执行中了?
但是,任务是程序员所定义的,所以就需要虚拟线程来封装任务 ,而线程只关心虚拟线程即可,也就是线程负责来调度各个虚拟线程的执行,也就是来判断虚拟线程是不是睡眠了?是不是正在运行?
我们可以把虚拟线程理解为一个对象,这个虚拟线程对象有几种状态,比如是不是睡眠中,是不是运行中,而一个线程可以支持同时运行多个虚拟线程对象,当线程发现某个虚拟线程对象睡眠时,就会去运行其他的虚拟线程对象。
或者这么说:虚拟线程sleep了,底层的线程并不一定sleep了。
所以,虚拟线程就是线程调度的单位,一个线程可以调度很多个虚拟线程 ,如果有多个线程,那当然就能调度更多虚拟线程了,所以在JDK的实现中,使用ForkJoinPool来提供线程作为虚拟线程的调度者 ,同时由于ForkJoinPool的任务窃取机制,能进一步提高任务并行执行的效率。
默认情况下,这个ForkJoinPool中的线程数等于CPU核心数,我们可以通过以下参数来修改:
- -Djdk.virtualThreadScheduler.parallelism=1
- -Djdk.virtualThreadScheduler.maxPoolSize=1
另外,虚拟线程也不用考虑池化,因为它不像线程,开启和关闭一个线程是需要调用操作系统的,而虚拟线程跟操作系统没关系。
再另外,JDK21中的虚拟线程也支持ThreadLocal,也就是一个虚拟线程在执行任务的过程中,也可以通过ThreadLocal来共享数据,使得我们在开发过程中就把虚拟线程当作普通线程使用就可以了。
还有要注意的是,当任务进行网络IO、磁盘IO时也相当是sleep了,所以如果虚拟线程用到真实项目中,就能做到用少量线程支撑较高的并发,从而能大大提高项目的吞吐量,虚拟线程不是用来提速的,而是用来提高吞吐量的。
好了,看到这里,你是不是对JDK21中的虚拟线程又有了新的理解呢?欢迎大家关注我的公众号:Hoeller,及时接收最新文章。