Java21虚拟线程学习+OS复习

31 阅读5分钟

虚拟线程

首先回顾一下,现代操作系统已知有3种并发维度,进程、线程、协程。 为什么会有这3个?为了并发。(快速在程序之间切换可以让所有的程序在某一个时间段内看起来都在运行)

为什么会设计3种?

进程

首先进程:

  • 有单独的地址空间
  • 进程之间也相互隔离,互不影响。

坏处:

  • 创建开销大(需要单独创建地址空间)
  • 切换开销大(用户态<->内核态转换、虚拟内存页表+TLB刷新)
  • 因为地址隔离,所以通信不便。 【TLB 是 CPU 用来加速虚拟地址转物理地址的缓存。一旦 TLB 失效,接下来的内存访问都要去查慢速的物理内存,性能会瞬间暴跌。】 【什么是用户态和内核态,为什么需要区分?CPU是2种不同的运行级别。用户态指的是我们自己写的代码运行的应用程序,内核态是操作系统底层程序,包括读写文件,读写网卡等】 【这样做是为了防止用户态存在的恶意程序,导致整个操作系统崩溃】

线程

为了提升效率、也为了让通信更简单,因此有了线程:

  • 在同一个进程内,空间共享,因此通信效率快,
  • 且无需切换内存刷新页表。

坏处:

  • 还是涉及上下文切换。

那么目前看来只要优化这个上下文切换就好了,能更快。那么协程就是干这个事情的。

协程

1个线程可以有多个协程,每个协程无需操作系统切换,由程序语言支持(如Go的协程、Java的虚拟线程),内部自行切换就行(也就是用户态自行操作,不涉及操作系统,操作系统不可见)。这样就不用劳烦操作系统了。

另外关于内存的问题,操作系统为了让线程通用,因此会给他分配不少的内存 1MB-8MB(取决于配置);但是协程大部分情况下用不到这么多,而且语言系统可以大致预估占用多少,因此基本空间都在2k-4k,超过了再申请。如下,因此我们可以创建大量的线程: 【

  • 线程大是因为操作系统为了兼容 C 语言指针和防止溢出,必须保守预分配。
  • 协程小是因为语言运行时(Runtime)懂代码,敢于按需分配,并且能搞定**搬家(移动栈)**带来的指针修正问题。
  • 扩容慢是事实,但依靠概率优势(绝大多数任务都很浅)掩盖了这个问题。
  • 状态存储都在堆内存(Heap)上,申请和访问都是纯用户态操作,无需惊动内核,所以快到飞起。 】

那它有没有问题呢?有: 因为在同1个线程内,1个协程调用了非协作式阻塞操作(比如传统的 Thread.sleep,会导致整个线程里的协程阻塞,因为阻塞了线程,1个线程内的协程当然就应该都被阻塞,操作系统不感知协程的)【协程一般有自己的阻塞操作,比如var data = await db.query();】(PS:Java21/其他编程语言已经做了此类兼容优化,一般不会实际进行阻塞,而是会切换到其他线程,除非在 synchronized 块或者 native 方法中调用了阻塞操作)

在这里插入图片描述


在这里插入图片描述

结构图示

在这里插入图片描述

作用场景

  • 如果你的任务是计算密集型(不仅要算,还要隔离),用进程(如 Chrome 每个 Tab)。
  • 如果你的任务是计算密集型(需要多核并行),用线程(如 视频编码、AI 推理)。
  • 如果你的任务是IO 密集型(疯狂的网络请求、读写数据库),用协程(如 网关、爬虫、即时通讯),这是目前高并发领域的最佳实践。
  1. 为什么“计算密集 + 强隔离”要用进程?(如 Chrome Tab) 因为进程之间内存是完全隔离的。以Chrome Tab为例子,1个挂了不影响其他的。而且可以防止A网站偷取B网站的信息。

  2. 为什么“计算密集 + 多核并行”要用线程(如 视频编码、AI) 因为此时的问题是CPU跑满了,一直在算,而且要大家一起算,共同完成1个任务(如 线程 A 处理上半张图,线程 B 处理下半张图,它们读的是同一块内存地址,一起完成视频编码)。 而且CPU调度的最小单位就是线程,因此8核CPU,开辟8个线程,能让每个CPU都跑满。

  3. 为什么“IO 密集型”要用协程?(如 网关、爬虫) IO密集型CPU基本都在等待,等文件系统、等网络传输,基本不用干活。 在处理这种场景情况下的大量链接有效,比如:网关,要维持很多链接,1个协程处理1个连接,可以处理很多,但是线程就需要更大的存储、切换开销也大。

    CPU 的速度是纳秒级(ns),而网络/硬盘的速度是毫秒级(ms)。 1 毫秒 (ms) = 1,000,000 纳秒 (ns)。 CPU 执行一条指令只需要 0.x 纳秒。 读取一次网络数据通常需要 50 - 200 毫秒。

如何创建

4种方式

使用 Thread.startVirtualThread() 创建 使用 Thread.ofVirtual() 创建 使用 ThreadFactory 创建 使用 Executors.newVirtualThreadPerTaskExecutor()创建

首先有一个Runnable:

static class CustomThread implements Runnable {
  @Override
  public void run() {
    System.out.println("CustomThread run");
  }
}

然后是4种方式:

Thread.startVirtualThread(new CustomThread());

Thread.ofVirtual().unstarted(new CustomThread()).start();
Thread.ofVirtual().start(new CustomThread());

Thread.ofVirtual().factory()
	.newThread(new CustomThread())
	.start();

Executors.newVirtualThreadPerTaskExecutor()
	.submit(new CustomThread());

性能对比参考:虚拟线程和平台线程性能对比

在这里插入图片描述 2千个线程才达到和20个虚拟线程差不多的处理速度