JDK19推出了几个新的特性,其中最具有特点的有以下几个。
图片
我们看到其中有一个比较值得关注的那就是新增了虚拟线程。到底什么是虚拟线程,和我们现在使用的线程有啥区别呢?
线程的实现方式
在操作系统中,线程是比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。
线程的实现方式主要有三种:分别是使用内核线程实现、使用用户线程实现以及使用用户线程加轻量级进程混合实现。
「使用内核线程实现」:内核线程(Kernel-Level Thread,KLT)直接由操作系统内核支持,由内核完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上,同时向应用程序提供API接口来管理线程。应用程序一般不直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP)。
应用程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。
有了内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作。
但是轻量级进程具有它的局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
「使用用户线程实现」:在用户空间建立线程库,通过运行时系统完成线程的管理。这种实现方式下,一个进程和线程之间是一对多的关系。其优点是线程切换快,并且可以运行在任何操作系统之上,只需要实现线程库即可。
这种实现方式下,一个进程和线程之间的关系是一对多的。
这种线程实现方式的优点是线程切换快,并且可以运行在任何操作系统之上,只需要实现线程库就行了。但是缺点也比较明显,就是所有线程的操作都需要用户程序自己处理,并且因为大多数系统调用都是阻塞的,所以一旦一个进程阻塞了,那么进程中的所有线程也会被阻塞。还有就是多处理器系统中如何将线程映射到其他处理器上也是一个比较大的问题。
「使用用户线程加轻量级进程混合实现」:这种实现方式结合了上述两种方式的优点,既具有用户线程实现方式的线程切换快的优点,也具有内核线程实现方式的操作系统支持性好的优点。
「Java线程的实现方式」Java作为一门跨平台的编程语言,实际上他的线程的实现其实是依赖具体的操作系统的。而比较常用的windows和linux来说,都是采用内核线程的方式实现的。也就是说,当我们在JAVA代码中创建一个Thread的时候,其实是需要映射到操作系统的线程的具体实现的,因为常见的通过内核线程实现的方式在创建、调度时都需要进行内核参与,所以成本比较高,尽管JAVA中提供了线程池的方式来避免重复创建线程,但是依旧有很大的优化空间。而且这种实现方式意味着受机器资源的影响,平台线程数也是有限制的。
虚拟线程
JDK19引入的虚拟线程,虚拟线程是轻量级线程,主要用于实现高吞吐量的并发应用程序。与传统的线程相比,虚拟线程具有更低的创建和销毁成本,可以更好地利用系统资源。
图片
虚拟线程的主要优势:
「虚拟线程与平台线程的区别」:
「如何使用虚拟线程」:通过Thread.startVirtualThread()可以运行一个虚拟线程:
Thread.startVirtualThread(() -> {
System.out.println("虚拟线程执行中...");
});
通过Thread.Builder也可以创建虚拟线程,Thread类提供了ofPlatform()来创建一个平台线程、ofVirtual()来创建虚拟现场。
Thread.Builder platformBuilder = Thread.ofPlatform().name("平台线程");
Thread.Builder virtualBuilder = Thread.ofVirtual().name("虚拟线程");
Thread t1 = platformBuilder .start(() -> {...});
Thread t2 = virtualBuilder.start(() -> {...});
线程池也支持了虚拟线程,可以通过Executors.newVirtualThreadPerTaskExecutor()来创建虚拟线程:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
其实并不建议虚拟线程和线程池一起使用,因为Java线程池的设计是为了避免创建新的操作系统线程的开销,但是创建虚拟线程的开销并不大,所以其实没必要放到线程池中。
「性能差异」从Runnable创建10000个线程,并使用虚拟线程和平台线程执行它们,以比较两者的性能。
final AtomicInteger atomicInteger = new AtomicInteger();
Runnable runnable = () -> {
try {
Thread.sleep(Duration.ofSeconds(1));
} catch(Exception e) {
System.out.println(e);
}
System.out.println("Work Done - " + atomicInteger.incrementAndGet());
};
传统线程实现:
Instant start = Instant.now();
try (var executor = Executors.newFixedThreadPool(100)) {
for(int i = 0; i < 10_000; i++) {
executor.submit(runnable);
}
}
Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();
System.out.println("总耗时 : " + timeElapsed);
虚拟线程实现:
Instant start = Instant.now();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for(int i = 0; i < 10_000; i++) {
executor.submit(runnable);
}
}
Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();
System.out.println("总耗时 : " + timeElapsed);
最终结果:
总耗时 : 102323
总耗时 : 1674
100秒和1.6秒的差距,虚拟线程的性能提升还是比较大的。
Java虚拟线程相对于传统线程具有更好的性能表现,特别是在高并发场景下。但是需要注意的是,虚拟线程并不是万能的,过度使用虚拟线程也可能会导致其他问题,如线程数量过多、线程间通信和同步问题等。