前阵子发布的 Java 19 正式版本,在众多的新特性当中,让小年最受瞩目的是: JEP 425: Virtual Threads(虚拟线程)
What?可能有人的关注点是:现在都到了 Java19了?劳资还在用 java8 ...
没错!java 20 也在路上了...
言归正传,虚拟线程又是什么鬼呢?其实说白了就是协程。都2022年了,该不会还有人不知道协程吧?
熟悉 Python 和 Go 的同学应该对协程并不陌生,但对于 Java 开发者来说,期待可太久了,可谓是千呼万唤始出来。
而且这也是个高频的面试题,想当年小年就被这个给问倒过。
线程
从实现方式的维度来划分,线程分为:
- 内核态线程
- 用户态线程
我们经常说的多线程,更多指的是内核态线程。内核态线程跟操作系统的内核线程是一一对应的,在很多编程语言层面上的实现,其实底层是调用了操作系统的函数。
所以线程的整个生命周期包括创建、运行、阻塞、销毁,这部分核心操作实际上都是交由操作系统来控制实现的。
那么,是不是说创建越多的线程,就能越加提高应用程序的处理能力呢?
答案是否定的,线程并不能无限创建,线程并发的处理能力是取决于 CPU 的内核线程数。
- 当线程数小于 CPU 的内核线程数,各个线程互相不抢占 CPU 线程资源,可以同时进行且是最高效的,这叫并行。
- 但是当线程数超过了 CPU 的内核线程数,多个线程就会抢占 CPU 线程资源,而 CPU 线程一个时刻只能运行一个线程,所以这些线程只能是分时交替执行,宏观上给人感觉有多个在同时运行,这叫并发。
内核线程的创建、销毁、切换是高成本的操作,而且对于线程的切换还必须考虑到:
- 线程切换的时机由操作系统决定(抢占式),线程无法对切换时机做任何假设。因此,多线程程序开发时必须考虑竞态
- 线程切换时涉及到特权级的跳转和线程上下文的保存/载入
所以 Java 的开发者同学应该都熟悉,在多线程业务场景中很多时候我们更多的是以线程池的方式来实现。
因此,协程的出现,正是为了解决线程所面临的这些问题和局限。
协程
协程 (Coroutine),其实也是上面所说的用户态线程的一种。
怎么定义这个用户态线程?线程的操作是取决于操作系统的,而协程是基于线程之上,是更为轻量级的一种。一般来说由编程语言来提供实现,在线程上的多个协程通过协作的方式来进行切换,而不是像线程那样,通过抢占式的方式来占取调度资源。
简单理解,就是可以在一个线程中定义多个协程(或者可以类比为任务),而协程之间会通过协作方式来获取线程的资源,比如其中的一个协程读取数据库,会发生 IO 阻塞,那么当前的线程资源就会去执行其他协程。
我们看一下这个图就很清晰,线程跟协程一对多的关系:
再举个例子,比如在一个业务场景中,你需要查询两个互不依赖的 SQL。在一般的情况下,我们为了提高响应效率,通常都会在当前线程执行一个 SQL-A,开一个异步线程执行另外一个 SQL-B。然而实际上,两个线程大多数的时间都是在等待数据库的响应,都处于阻塞等待的状态中。而且这两个线程有可能使用同一个内核线程,还需要不断地切换上下文执行等待。
而这时候,协程就必须得安排上了。把用异步线程执行 SQL-B,用协程来代替。你可以通过编程语言的 API 去判断协程中的 SQL 是否执行完毕,这就有点像 Java 中的 Future.get()
。那么整个业务场景其实就只会占用一个线程的资源,因为不管是 SQL-A 还是 SQL-B 的 IO 阻塞都是避免不了的,但是使用协程的方式,可以用更少的线程资源完成。
两者对比
- 实现层面上,线程是操作系统的资源,由操作系统调度执行。而协程是在线程之上的,属于用户态线程,一个线程可以创建多个协程。由编程语言提供实现,开发者可以自由调度操作。
- 复杂度,协程之间的切换,也是需要涉及上下文和栈的操作,在实现层面上来说会更为复杂一些。当然编程语言都已经帮我们做好了。
- 执行效率,协程的切换效率比线程的切换更高。协程调度切换时,将寄存器上下文和栈保存到线程的堆区,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
编程语言哪家强
其实对于协程的实现,很多编程语言都已经支持了。比如 Python、Go 早已经原生内嵌了。而 Java 作为小年主要开发语言,看到 Java19 支持协程的特性,自然是会兴奋一点。
当然为了对比各语言的实现特性,小年把压在箱底的 Python 和 Go 复习了一把,并写了 very very easy 的例子来加深一下大家的印象。Demo源码已放上Github:https://github.com/Zhang-BigSmart/awesome-coroutine
Python
在 python3.4 之前,使用三方库实现,比如:
- Gevent
3.4之后官方内置了 asyncio 标准库,真正支持协程特性
-
asynico + yield from(python3.4)
-
asynico + await(python3.5)【推荐】
Golang
Go 语言,从语言层面天生支持并发,可以说是为并发而生的语言。对于网络并发编程来可谓是性能利器,这也是为什么很多比如像 Google、Facebook、七牛云、Bilibili 等这些公司首选的编程语言之一。
用法相比于 python 来说,更为简洁方便。
// 执行一个函数*
func()
// 开启一个协程执行这个函数*
go func()
Java
Java 19终于支持支持协程了,只不过官方的是叫虚拟线程。第一步需要大家先安装配置好 Java 19 的环境。
新API:
- Thread.ofVirtual():虚拟线程,也就是协程
- Thread.ofPlatform():平台线程,也就是我们现在常用的线程
// Thread.getId() from jdk19 abandoned
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
Thread thread = Thread.ofVirtual().name("testVT").unstarted(runnable);
Thread testPT = Thread.ofPlatform().name("testPT").unstarted(runnable);
testPT.start();
快速创建和启动虚拟线程:
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
Thread thread = Thread.startVirtualThread(runnable);
Executors.newVirtualThreadPerTaskExecutor()
虚拟线程池:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("hello"));
}
因为虚拟线程是预览特性,需要用
javac --release 19 --enable-preview Main.java
编译程序,再用java --enable-preview Main
运行程序
或者使用java --source 19 --enable-preview Main.java
运行程序
那么协程究竟有多块呢?
简单对平台线程&虚拟线程做了一下性能测试:并行执行 1w 个休眠1秒的任务,比较总执行时间和使用的系统线程数,
本地测试结果:
// 平台线程
207 os thread
platform elapsed time: 50208ms// 虚拟线程
20 os thread
virtual elapsed time: 1162ms
然而对于 Java 开发者来说要真正的能用上协程,估计还需要一段很长的时间。毕竟现在大多数用的还是最经典的 Java8。不过好消息的是即将发布的 SpringBoot 3.0,宣称最低支持的JDK 版本是 java17,有 springboot 的buff加成,用上 协程指日可待了。
上面就是小年对协程的一些学习总结了。
简单来说,协程,很大程度上将计算机资源发挥到淋漓尽致,提供最大化最高效的执行效率。显然它也必然是未来的一个发展趋势。
都 2022 年了,协程还不赶紧学起来!
普通的改变,将改变普通
我是宅小年,一个在互联网低调前行的小青年
关注公众号「宅小年」,个人博客 📖 edisonz.cn,阅读更多分享文章