如何控制java虚拟线程的并发度?
jdk 21中的虚拟线程已经推出好一段时间了,确实很轻量,先来一段示例:
假如有一段提交订单的业务代码:
1 public void submitOrder(Integer orderId) { 2 sleep(1000); 3 System.out.println("order:" + orderId + " is submitted"); 4 }
这里我们用sleep来模拟,每提交1个订单,大致耗时1秒。
如果有10个订单一起下单,顺序执行的话,至少10秒,很容易想到用多线程:
1 @Test 2 public void submitOrder1() { 3 ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("submitOrder-pool-%d").build(); 4 ExecutorService submitOrderPool = new ThreadPoolExecutor(4, 16, 300L, TimeUnit.SECONDS, 5 new LinkedBlockingQueue<>(128), threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy()); 6 CompletableFuture<Void>[] futures = new CompletableFuture[10]; 7 StopWatch watch = new StopWatch(); 8 watch.start(); 9 //下10个订单 10 for (int i = 0; i < 10; i++) { 11 final int orderId = i; 12 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> submitOrder(orderId), submitOrderPool); 13 futures[i] = future; 14 } 15 CompletableFuture.allOf(futures).join(); 16 watch.stop(); 17 System.out.println("Time: " + watch.getTime()); 18 }
这里我们使用了传统的线程池,核心线程数是4,小于10,所以会内部排队,最终执行效果大致如下:
order:0 is submitted order:3 is submitted order:1 is submitted order:2 is submitted order:4 is submitted order:6 is submitted order:7 is submitted order:5 is submitted order:8 is submitted order:9 is submitted Time: 3035
明显快多了,但是占用了4个传统的java线程。
我们改用虚拟线程来改写:
1 @Test 2 public void submitOrder2() { 3 CompletableFuture<Void>[] futures = new CompletableFuture[10]; 4 StopWatch watch = new StopWatch(); 5 watch.start(); 6 //下10个订单(这里使用虚拟线程) 7 try (ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) { 8 for (int i = 0; i < 10; i++) { 9 final int orderId = i; 10 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> submitOrder(orderId), service); 11 futures[i] = future; 12 } 13 CompletableFuture.allOf(futures).join(); 14 } 15 watch.stop(); 16 System.out.println("Time: " + watch.getTime()); 17 }
运行效果:
1 order:1 is submitted 2 order:3 is submitted 3 order:2 is submitted 4 order:4 is submitted 5 order:8 is submitted 6 order:9 is submitted 7 order:0 is submitted 8 order:5 is submitted 9 order:7 is submitted 10 order:6 is submitted 11 Time: 1035
对原有代码仅做了轻微改动,就能享受到虚拟线程的好处,占用资源更小,整体耗时更低,看上很完美!但真的这么丝滑吗?
这里有一个坑:正所谓“成也萧何,败也萧何”,虚拟线程这种轻量性的机制,导致它创建起来成本太低了,完全没有池化的必要,1台机器轻轻松松就可以瞬间创建上万甚至上百成的虚拟线程。相比传统的线程池机制(有coreSize,maxSize,workQueue缓冲,以及各种policy策略兜底),虚拟线程完全没有并发度控制的概念,如果瞬间生成大量的虚拟线程,每个里都是执行db/redis操作,很容易就把db、redis连接池打爆!!!
因此,实际生产应用中,强烈建议大家控制虚拟线程的执行并发数!!!
可以参考下面的做法,使用信号量来约束:
1 @Test 2 public void submitOrder3() { 3 CompletableFuture<Void>[] futures = new CompletableFuture[10]; 4 StopWatch watch = new StopWatch(); 5 watch.start(); 6 //下10个订单 7 try (ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) { 8 //限制并发数为4 9 final Semaphore POOL = new Semaphore(4); 10 for (int i = 0; i < 10; i++) { 11 final int orderId = i; 12 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { 13 try { 14 //获取信号量(注:信号量用完了,后面的任务就只能等着) 15 POOL.acquire(); 16 submitOrder(orderId); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } finally { 20 //执行完后,释放信号量 21 POOL.release(); 22 } 23 }, service); 24 futures[i] = future; 25 } 26 CompletableFuture.allOf(futures).join(); 27 } 28 watch.stop(); 29 System.out.println("Time: " + watch.getTime()); 30 }
1 order:2 is submitted 2 order:1 is submitted 3 order:0 is submitted 4 order:3 is submitted 5 order:4 is submitted 6 order:5 is submitted 7 order:8 is submitted 8 order:9 is submitted 9 order:7 is submitted 10 order:6 is submitted 11 Time: 3042
参考文档:
https://blog.moyucoding.com/jvm/2023/09/23/ultimate-guide-to-java-virtual-thread
https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-DC4306FC-D6C1-4BCC-AECE-48C32C1A8DAA
https://www.didispace.com/article/java-21-virtaul-threads.html
出处:http://yjmyzz.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。