前言

Java21去年发布后虚拟线程终于作为了release特性,本来一直想看看的,结果一年都要过去了还没有取好好看过,说是尝鲜,现在Java24都要发了,黄花菜都凉了。正好没事大概看看咋用吧。

虚拟线程

虚拟线程和传统线程有一些关键的区别,各自都有其优势,适用于不同的应用场景。以下是详细的对比和说明:

关键区别

  1. 轻量级

    • 虚拟线程:虚拟线程非常轻量,可以创建成千上万个线程,而不会显著增加内存和CPU负担。
    • 传统线程:传统操作系统线程相对较重,创建和管理成本较高,处理大量并发任务时可能导致性能问题。
  2. 调度与管理

    • 虚拟线程:由JVM直接管理,优化了线程调度和资源利用,减少了操作系统上下文切换的开销。
    • 传统线程:由操作系统管理,线程调度和管理较为复杂,上下文切换开销较大。
  3. 编程模型

    • 虚拟线程:允许使用传统的阻塞式编程模型,简化了代码编写和维护。
    • 传统线程:虽然也支持阻塞式编程,但在高并发场景中通常需要使用异步编程和回调机制来提高性能。

各自的好处

虚拟线程的好处:

  • 高并发能力:可以轻松处理大量并发任务,适用于I/O密集型应用,如高并发的网络服务、消息处理等。
  • 简化编程:允许使用更直观的阻塞式编程模型,减少了异步编程的复杂性。
  • 更好的资源利用:由JVM优化管理,能够高效利用系统资源。

传统线程的好处:

  • 成熟稳定:经过多年的优化和验证,具有很高的稳定性和可靠性。
  • 操作系统集成:与操作系统的调度和资源管理机制紧密集成,适用于需要高优先级和实时响应的任务。
  • 复杂调度需求:适用于需要精确时间调度和资源控制的应用,如实时系统。

适用场景

虚拟线程适用场景

  • 高并发网络应用:如Web服务器、消息队列处理、微服务架构等。
  • I/O密集型任务:如文件读写、大量网络请求处理等。
  • 简化并发代码:希望减少异步编程复杂性,提高代码可读性和可维护性的应用。

传统线程适用场景

  • CPU密集型任务:如高性能计算、数据处理等。
  • 需要高优先级调度的任务:如实时系统、后台服务、系统级任务等。
  • 复杂调度和资源控制:如多线程游戏引擎、硬实时控制系统等。

我的理解,其实几点:

  • 虚拟线程(协程)是进程(JVM)自己控制的,传统的线程则是由系统内核控制的,因而传统线程的创建和销毁,上下文切换,天然就比较耗时
  • 用户线程和系统线程之间存在着相关的映射关系,这个貌似讲起来比较复杂,但是Java的线程应该是采用1:1的映射关系,协程(虚拟线程)就是N:M的关系了。从这个角度来说协程就是不是很重要的资源了,也没必要进行池化了。

Java虚拟线程

写了个demo,虚拟线程实现了原有的线程框架,用起来貌似没太多新方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
* test1
*/

public class Test {

void test1(){

Thread.startVirtualThread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("虚拟线程测试1");
});
}

void test2(){
Thread.ofVirtual().name("MyVirThread2")
.start(() -> {
System.out.println("虚拟线程测试2");
});
}

void test3(){
try (ExecutorService virtualThreadPerTaskExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100; i++) {
int finalI = i;
virtualThreadPerTaskExecutor.submit(() -> {
System.out.println(STR."虚拟线程池测试 \{finalI}");
});
}
}
}

void test4(){
ThreadFactory threadFactory = Thread.ofVirtual().name("VirFactory",1).factory();
for (int i = 0; i < 100; i++) {
threadFactory.newThread(() ->{
System.out.println(STR."虚拟线程测试 \{Thread.currentThread().getName()}");
});
}

}

void test5(){
new Thread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("传统线程测试");
}).start();
}

}

问题发现

运行的时候发现了一个有意思的事情:执行协程方法,发现控制台并没有打印东西,但是换成test5(),传统线程来执行,又有相关打印,在虚拟线程里面加入sleep阻塞,也没有用。其实问题很简单,就是协程还没执行完,主线程就执行完毕,进程直接结束了原因。之前学GO的时候也遇到这个问题,上demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package demo

import (
"fmt"
"time"
)

func Test5() {

done := make(chan struct{}, 1)

go func() {
time.Sleep(5 * time.Second)
fmt.Println("5秒后协程执行完了")
done <- struct{}{}
}()

<-done //阻塞住主线程

fmt.Println("主线程已经结束")

}

解决的方法也就是在阻塞住主线程,等协程执行完之后才释放,Java的话用CountDownLatch也能实现类似的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {

void test1(CountDownLatch countDownLatch){

Thread.startVirtualThread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("虚拟线程测试1");
countDownLatch.countDown();
});
}
}

调用处:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
Test test = new Test();
CountDownLatch countDownLatch = new CountDownLatch(1);
test.test1(countDownLatch);
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

不过和GO的写法比起来还是比较丑陋啊。