前言
Java21去年发布后虚拟线程终于作为了release
特性,本来一直想看看的,结果一年都要过去了还没有取好好看过,说是尝鲜,现在Java24都要发了,黄花菜都凉了。正好没事大概看看咋用吧。
虚拟线程
虚拟线程和传统线程有一些关键的区别,各自都有其优势,适用于不同的应用场景。以下是详细的对比和说明:
关键区别
轻量级:
- 虚拟线程:虚拟线程非常轻量,可以创建成千上万个线程,而不会显著增加内存和CPU负担。
- 传统线程:传统操作系统线程相对较重,创建和管理成本较高,处理大量并发任务时可能导致性能问题。
调度与管理:
- 虚拟线程:由JVM直接管理,优化了线程调度和资源利用,减少了操作系统上下文切换的开销。
- 传统线程:由操作系统管理,线程调度和管理较为复杂,上下文切换开销较大。
编程模型:
- 虚拟线程:允许使用传统的阻塞式编程模型,简化了代码编写和维护。
- 传统线程:虽然也支持阻塞式编程,但在高并发场景中通常需要使用异步编程和回调机制来提高性能。
各自的好处
虚拟线程的好处:
- 高并发能力:可以轻松处理大量并发任务,适用于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;
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
的写法比起来还是比较丑陋啊。