发布时间:2023-09-24
统计字数:1568 字
阅读时间:8 min read
网上已经有很多关于虚拟线程的介绍了,这里我就不复制粘贴过来,有兴趣可以去看看大佬们的介绍,我就说一下我认为的虚拟线程吧。
首先我们在背八股文的时候都背过进程和线程的区别,进程是操作系统中的一个独立执行单位,而线程则是进程内的一个执行单位,进程之间是相互独立的,而同一进程的线程则可以通过共享内存空间来实现通信。
而比较关键的一点则是 进程和线程相比,线程创建,切换和销毁的代价较低,占用的系统资源较少。
同时我们在程序开发上就经常使用到线程这种东西,例如我们想在不阻塞主线程执行的情况下去执行其他的一些操作,例如发送消息,上传文件等等,我们就可以创建一个新线程去进行操作。
但就算是线程,我们如果频繁的创建,销毁,它的一个性能影响依旧是很大的,这里又涉及到一些关于操作系统的知识了,总之频繁的创建和销毁线程相比于操作进程而言是一个轻量的操作,但依旧对性能有影响。
所以在 Java 中,我们通常会创建一个线程池来进行线程的操作,也就是池化技术,通过这种技术来最大限度减少线程操作的损耗,例如 Tomcat 的连接池,Mysql 的连接池。
我们在开发一些 IO 耗时较长的逻辑的时候,也会使用线程池来有效增加这个吞吐量。但是这里终究有个问题,就是线程池中的线程始终是有限的,它并不是 Jdk 来实现线程的操作逻辑,而是会调用系统接口来创建一个真实的线程。
而我们看一个 Go 语言中相比于 Java 出色的一点就是 Go 拥有协程。
Go语言的协程(Goroutine)是一种轻量级的线程,由Go语言的运行时系统管理。协程是Go语言并发编程的重要组成部分,它们允许你在程序中并发执行代码,而不需要显式地创建线程和管理线程的生命周期。协程是Go语言中并发的基本单元,它们相比传统的线程更加高效,因为它们可以在少量的内存上运行,而且创建和销毁协程的开销很小。
Go语言的协程与传统线程的主要区别在于:
- 轻量级:Go协程使用的内存远比传统线程少,因此可以创建成千上万个协程而不会导致系统资源耗尽。
- 并发性:Go协程使得并发编程变得容易,你可以轻松创建和管理协程,而不需要担心线程同步和锁定等复杂问题。
- 通信:Go协程之间可以通过通道(Channel)进行通信,这是一种用于在协程之间传递数据的机制。通道可以安全地传递数据,避免了共享内存导致的竞态条件和死锁问题。
从这段简介就知道,Go的协程它跟线程的作用是一样的,但是它却是依赖于 Go 语言本身来实现,并不是系统级别。它可以很轻易地创建成百上千万个协程来操作,并且资源消耗极低,这种在 Jdk 虚拟线程出来之前是根本实现不了的。
这也是为什么这么多音视频,网盘和高 IO 并发等等应用程序选择使用 Go 来实现的原因,因为实现并发操作极其简单,性能又好。
但是在 Jdk21 后,Java 拥有了虚拟线程,这意味着 Java 也能做到这些了。
我们可以拿一个很小的 Demo 来看一下虚拟线程性能,先来看一下这个 Demo。
public class VirtualThreadDemo {
public static void main(String[] args) {
var a = new AtomicInteger(0);
// 创建一个固定200个线程的线程池
try (var vs = Executors.newFixedThreadPool(200)) {
List<Future<Integer>> futures = new ArrayList<>();
var begin = System.currentTimeMillis();
// 向线程池提交1000个sleep 1s的任务
for (int i = 0; i < 1_000; i++) {
var future = vs.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return a.addAndGet(1);
});
futures.add(future);
}
// 获取这1000个任务的结果
for (var future : futures) {
var i = future.get();
if (i % 100 == 0) {
System.out.print(i + " ");
}
}
// 打印总耗时
System.out.println("Exec finished.");
System.out.printf("Exec time: %dms.%n", System.currentTimeMillis() - begin);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}
这一段代码是从网上中找来的,其内容其实就是创建一个固定核心线程数为 200 的线程池,然后多线程去操作原子类自增,同时逻辑里面还添加了 Thread.sleep(Duration.ofSeconds(1));
休眠一秒,来模拟耗时操作。
我先执行看看其执行速度是多少。
一共执行了5秒,也就是1000个任务,每个任务都要休眠1秒,而线程池为200核心线程数,200 * 5 = 1000,这也是符合计算结果。
那我们来看看虚拟线程的执行结果,将 var vs = Executors.newFixedThreadPool(200)
换成 var vs = Executors.newVirtualThreadPerTaskExecutor()
,不需要添加任何的参数。
执行时间只需要 1s !
也就是相当于有 1000 个线程在同时执行,并且其实这么多虚拟线程只需要一个平台线程来进行调度即可。
尽管 Jdk21 已经是正式版本了,不止拥有虚拟线程,还拥有比较先进的垃圾收集器,但是我们在企业中依旧大范围使用的是 1.8 版本,甚至有一句话,你强任你强 清风拂山岗,他发由他发,我用Java8。
但是现在情况可能要发生一点改变了,Jdk21 那么多先进的改动,以及 SpringBoot3 最低要求的 Jdk17,我相信随着时间推移,新项目会一步步用上最新版本的 Jdk。
放着这么好用的东西缺用不了,实在是可惜。