虚拟线程

虚拟线程是jdk19提出来的预览特性,jdk21转正。在以往的一对一线程模型中,虽然将进程划分为了线程,但是线程的粒度还是太大了,一个线程占1m,在高并发动不动几十万qps下,内存都不一定打的住。利用粒度更细的协程,可以将这部分开销继续缩小。

虚拟线程优点

  1. 占据资源少,切换开销小:虚拟线程是一种用户级线程(绿色线程),不需要操作系统管理而是jvm管理。切换开销更小,可能只涉及jvm的堆空间。
  2. 增大cpu的吞吐率,适合io密集型任务:在经典的线程模型中,线程在io时会让出cpu。如果每个线程都要io那么cpu的利用率就很低了,会导致资源耗尽的同时硬件资源没有充分利用。但是虚拟线程是运行在平台线程上的,一个虚拟线程io了可以切到另一个虚拟线程,但是平台线程是不会释放的。这样可以用粒度更低的开销更高效的利用线程资源。
  3. 异步改同步,适合阻塞式io:io多路复用是通过一个线程来监听多个socket的状态,通过大量的异步回调实现,机制较为复杂;虚拟线程跟它解决的问题是一致的,都是解决如何增大并发量;但是虚拟线程不需要进行如此复杂的异步回调,用同步操作即可,如果一个线程在io阻塞,jvm无非就是把平台线程的资源切到另一个虚拟线程,免去了复杂的回调。
  4. 更灵活:有个例子我觉得很形象。传统的线程模型可以理解为一排固定死的插座,虚拟线程模型就是插座连接的插排。你要修改插座是需要改墙体改布线的,对应os和cpu的关系,而且还存在就算插排插满了但是功率还上不去的现象;但是如果用虚拟线程也就是插排,你想要多点接口就换个更大的插排,这样你的功率也上去了,对电力的利用率也很高。

select/poll/epoll

  1. select: 每一个socket对应一个fd(文件描述符),用户态给内核态监听,最多长度1024,内核态通过循环的方式监听,一旦有io准备就绪就改变fd状态,然后传输给用户态的时候应用进程还需要遍历一边。也就是说既有长度限制,有需要遍历两次,效率较低。
  2. poll:用链表改变了1024这个数字,其余没区别。还是改变状态让用户态自己去遍历。
  3. epoll:将socket的文件描述符通过红黑树组织起来,查询效率从on变为ologn,此外将循环遍历变成事件回调,有复杂的事件注册函数。触发机制有水平触发和垂直触发,一旦回调函数说socket准备就绪,就把他放在一个队列里面等待返回用户态,水平触发是只要没读完就一直提醒用户态来读,垂直触发是只提醒一次,但是一次需要全部读完。

java虚拟线程和go的gmp协程模型

gmp模型

  1. g是goroutine,每次在调用go func的时候就会产生一个g,也可以理解为协程控制块;m是机器线程,对应了一个真实的被操作系统调度的线程;p是逻辑处理器,必须要一个g通过p绑定到m才可以运行,相当于一个上下文填充器。
  2. 有两个队列:局部运行队列LRQ,每一个p都有一个这个队列,放等待该p作为中介给m运行的g;全局运行队列GRQ,分配尚未给LRQ的m
  3. gmp模型是通过调度的方式把g调度给线程,底层运行的还是线程,是一个M对N模型,这点与java是不同的,java的虚拟线程是通过jvm调度的,运行在平台线程上,而不是哪里线程空了就用哪个线程的资源。
  4. 交接机制:如果一个m上的g在等待io,这个时候中介p挂到别的空闲m上让自己队列里面g使用。使得阻塞不会影响整体的调度。
  5. 窃取机制:每个线程都是工贼,没有任务就要去别的LRQ里面偷,或者去GRQ中偷。

虚拟线程调度模型

  1. Java 的虚拟线程由 Continuation(保存/恢复执行栈)+ Scheduler(Executor) 实现:虚拟线程在运行时被 mount(挂载)到一个 carrier(承载,平台)线程 上执行;遇阻塞时会 yield(保存 continuation 并释放 carrier),阻塞完成或 unpark 时将 continuation 重新提交给 scheduler 以被某个 carrier 线程恢复执行。这个逻辑以 java.lang.VirtualThread + JVM/HotSpot 的 continuation 支持 + JDK core libs 的调度器协作实现。
  2. 虚拟线程也支持交接和窃取,这点是gmp学的
  3. 注意虚拟线程使用synchronized会将虚拟线程pin住,因为这里synchronized是监视器锁,锁住的是线程,如果在虚拟线程用这个会把平台线程锁住,无法参与调度。一般并发低没事,高了就会将虚拟线程退化为平台线程。因此要控制同步操作使用基于jdk的reentrantlock好一点。