本文涉及到的技术:虚拟线程、结构化并发、线程池、TheadLocal,对原理感兴趣的可以直接跳到原理部分。
虚拟线程是JDK19中引入的,JDK21正式发布,我们先来看看虚拟线程的几种用法,然后再来分析底层实现原理。
通过观察输出结果,就能知道当前运行Task
的是不是虚拟线程。
两者输出结果类似,为:
根据名字可以看出确实是用的VirtualThread,但似乎跟ForkJoinPool有关,后面会分析。
我们也可以通过以下方式来创建普通线程:
输出结果为:
还可以先得到一个ThreadFactory,然后来创建虚拟线程:
还有一种更简单的API:
在JDK21中还有一个新特性(预览版),叫做结构化并发,也会自动创建虚拟线程来运行代码,比如:
还有一种和线程池类似的使用方式:
以上代码中每个任务运行时都会开启一个虚拟线程,输出�结果为:
以上大概就是使用或创建虚拟线程的几种情况了,那到底什么是虚拟线程呢?它跟线程有什么关系?它跟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
,虽然只有一个线程,却达到了并行执行三个任务的效果,其原理就是上面所分析的:
这样就达到了一个线程并行执行三个任务的效果,从中,我们可以看到,线程需要知道:一个任务什么时候开始睡眠了,什么时候睡眠结束了,哪个任务还没开始执行,哪个任务已经在执行中了?
但是,任务是程序员所定义的,所以就需要虚拟线程来封装任务,而线程只关心虚拟线程即可,也就是线程负责来调度各个虚拟线程的执行,也就是来判断虚拟线程是不是睡眠了?是不是正在运行?
我们可以把虚拟线程理解为一个对象,这个虚拟线程对象有几种状态,比如是不是睡眠中,是不是运行中,而一个线程可以支持同时运行多个虚拟线程对象,当线程发现某个虚拟线程对象睡眠时,就会去运行其他的虚拟线程对象。
或者这么说:虚拟线程sleep了,底层的线程并不一定sleep了。
所以,虚拟线程就是线程调度的单位,一个线程可以调度很多个虚拟线程,如果有多个线程,那当然就能调度更多虚拟线程了,所以在JDK的实现中,使用ForkJoinPool来提供线程作为虚拟线程的调度者,同时由于ForkJoinPool的任务窃取机制,能进一步提高任务并行执行的效率。
默认情况下,这个ForkJoinPool中的线程数等于CPU核心数,我们可以通过以下参数来修改:
另外,虚拟线程也不用考虑池化,因为它不像线程,开启和关闭一个线程是需要调用操作系统的,而虚拟线程跟操作系统没关系。
再另外,JDK21中的虚拟线程也支持ThreadLocal,也就是一个虚拟线程在执行任务的过程中,也可以通过ThreadLocal来共享数据,使得我们在开发过程中就把虚拟线程当作普通线程使用就可以了。
还有要注意的是,当任务进行网络IO、磁盘IO时也相当是sleep了,所以如果虚拟线程用到真实项目中,就能做到用少量线程支撑较高的并发,从而能大大提高项目的吞吐量,虚拟线程不是用来提速的,而是用来提高吞吐量的。
好了,看到这里,你是不是对JDK21中的虚拟线程又有了新的理解呢?如果有,我需要正反馈,一定要帮我点赞、收藏、分享哦!
我准备运营自己的知识星球啦,知识星球以免费答疑、优化简历、讨论技术为主,另外还能帮助大家提升工作效率、快速拿到offer、认识更多大牛、学习更多技术。
如果您愿意直接付费加入,那当然也是可以的啦