早早就听说Go里的协程性能高编写还简单,大家也一直诟病java的多线程编写起来不够优雅;这几天java19终于把这个特性也塞进java了,于是趁假期在树莓派里装个JDK19来试试这个新特性到底怎么样。
Java中的协程
Java19中引入的协程官方名字叫“虚拟线程 virtual thread”;
下面是我对部分官方文档的蹩脚翻译:
摘要
隆重介绍Java平台的虚拟线程。虚拟线程是一种轻量级线程,可以大幅降低开发、维护、观测高并发程序的难度。
目标
- 允许以简单的一个请求一个线程(The thread-per-request style)风格编写web应用,同时拥有近乎最佳的硬件利用率。
- 允许现存的使用 java.lang.Thread API 的应用以最小的带价转换至虚拟线程
- 允许使用现有的JDK工具组对虚拟线程进行故障排除、调试和分析
非目标
- 我们不想移除传统线程及其子类或者是让现存应用无缝切换到虚拟线程
- 我们不想改变Java现有的基础并发模型
- 我们不想在Java语言或者是类库中提供一个新的数据并行处理框架,Stream API 仍然是最推荐的并行处理大数据的方法
一个请求一个线程
web应用一般需要并发处理相互隔离的用户请求,所以应用程序可以用一个线程来处理一个请求的整个生命周期。这种一个请求一个线程风格因为使用了平台的并发单位来表示应用的并发单位,所以十分易于理解、开发和维护。
web应用的并发量可以用利特尔法则进行衡量,简而言之就是如果我们不能降低处理单个请求的耗时,那么想要增强程序的并发能力就只能引入更多的线程。
不幸的是可用线程的数量是有限的,因为传统JDK线程只是操作系统线程的一个包装,而操作系统的线程是昂贵的,这就使得一个请求一个线程模式在实现过程中会有许多问题。如果每个请求都消耗一个线程(也就是一个OS线程),那么线程数通常就会成为一个短板,外在表现即CPU或网络链接耗尽。JDK当前的线程实现将应用程序的吞吐量限制在远低于硬件可以支持的水平,即使是引入线程池之后;尽管线程池可以帮助避免频繁创建线程的开销,但是仍不能增加最大线程数。
性能测试
开启虚拟线程
由于虚拟线程暂时还是预览功能,默认是关闭的,想要使用的话需要在javac和运行时加点参数,如图
理想性能
这里就写一段没有什么意义的代码,搞一万个线程用JMH来试试新引入的虚拟线程与传统线程能有多大的性能差距;
code
public class VirtualThreadWork {
//耗时方法
public int handler(int size){
try {
Random random = new Random(System.currentTimeMillis());
//模拟数据库curd等耗时操作
Thread.sleep(200L);
List<Integer> list = new ArrayList<>();
//一点无意义的逻辑 就像工作时会写的的代码
for (int i = 0; i < size; i++) list.add(random.nextInt());
Optional<Integer> res = list.stream().max(Comparator.comparing(Function.identity()));
return res.orElse(Integer.MIN_VALUE);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
JMH的BenchMark类
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = 2)
public class ThreadBenchMarkTest {
@Benchmark
public void testBench(){
try (var executors = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10000).forEach(i ->{
executors.submit(()->{
VirtualThreadWork virtualThreadWork = new VirtualThreadWork();
virtualThreadWork.handler(100);
});
});
}
}
@Benchmark
public void traditionTEstBench(){
try (var executors = Executors.newCachedThreadPool()) {
IntStream.range(0, 10000).forEach(i ->{
executors.submit(()->{
VirtualThreadWork virtualThreadWork = new VirtualThreadWork();
virtualThreadWork.handler(100);
});
});
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ThreadBenchMarkTest.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
结果
结果非常的amazing啊,虚拟线程效果确实非常惊人
实验平台 8代i5低压U
使用虚拟线程的时候CPU压力不大,并没有跑满
实际的OS线程也没有上涨很多,空闲时是三千左右,跑起来最多只达到四千七八的样子。
但是来到传统线程这边,CPU压力就非常大了,尽管我已经用了线程池,但正如官方文档中说的,线程池并不能很好的解决系统线程有上限这个问题
和预期一样,OS线程数也有暴涨
最后是JMH给出的报告,差距确实悬殊,
虚拟线程完成任务只需0.226s,传统线程需要1.431s
当然这里只能分出高下,时间的比值并没有太大意义,毕竟传统组已经达到机器极限了,分数一定是断崖式下降
实际性能
官方文档中既然也提到了协程的应用场景是web应用,那么我们就用springboot简单捏一个传说中的one thread pre request应用部署到服务器上,和传统服务压测比下看看谁的QPS更高。
code
@RestController
//@RequestMapping("/pressure")
public class TestController {
@Autowired
private TestService testService;
private ExecutorService executor;
@PostConstruct
public void init(){
this.executor = Executors.newVirtualThreadPerTaskExecutor();
}
@GetMapping(value = "/virtual_test")
public Response pressureTest() throws ExecutionException, InterruptedException {
//实例化一个future对象,传入的callable会去调用killTime方法并拿到一个返回值
FutureTask<Integer> killTimeFuture = new FutureTask<>(() -> testService.justKillTime(100));
executor.submit(killTimeFuture);
return Response.success(String.valueOf(killTimeFuture.get()));
}
@GetMapping(value = "/tradition_test")
public Response traditionTest(){
var res = testService.justKillTime(100);
return Response.success(String.valueOf(res));
}
}
代码中TestService就是模拟真正业务中各种耗时操作的service;
对比的两组分别是:
- 请求进来后用Future提交给VirtualThread 获取结果
- 传统的直接调用service方法去获取结果
结果
实验平台 树莓派3B
可以看到先进组在QPS400时CPU负载极高,吞吐量在300出头
但是反常的是传统组在QPS200时CPU负载反而更低一些,最后的吞吐量也是可以达到400多(截图放的不是极限)
所以,最后就得到了很反直觉的结果:传统方法比引入虚拟线程更快些;这和预期是完全相反的,总结一下可能有这几处引入的误差
- 对照组设置不合理,虚拟线程应该和传统线程池进行比较而不是和直接调service方法的传统写法比较
- 实验平台性能有限,后期再测发现树莓派的CPU负载再也不能飙到300%多,两组的吞吐量均有所下降
- 对one thread pre request理解有偏差,编码有一定问题
就这样吧,毕竟是个实验功能,试试玩玩得了,毕竟在java19中都不是默认开启的,真正用到生产环境怕是要等到猴年马月:hushed: :hushed:
突然想明白了,后面这个实际性能测试简直太儿戏了;
官方文档中所说的”The thread-per-request style” 明显是靠spring MVC内部来实现的……
所以如果想测试协程对于web应用的性能提升需要从spring开始进行改造才行……
JDK 8 养老了
生产环境是这样的→_→ 不过最近应该也有一点升级的趋势,估计JDK11以后会逐步用起来?
jdk11更新的需求并不明显, 短时间内应该是不会升级的, 大概率会从8直接升级到2x 的版本, 算是跨代升级了
主要是2x版本的协程从版本上来说, 解决了框架线程池(包含线程)开销特别大的问题, 对于吞吐量有比较明显的提升