一些杂想:Java老矣,尚能饭否?( 五 )


Loom 项目的目标是让 Java 支持额外的N:M 线程模型[46] , 实际上是将 JVM 线程与 OS 线程解耦 。Loom项目新增加一种用户态的“虚拟线程”( )[47] , 本质上它是一种有栈协程( )[48] , 多条虚拟线程可以映射到同一条物理线程之中 。
在此之前 , Java中已经有一些三方的实现支持协程 , 比如[49]和[50] , 貌似都是需要挂载agent利用字节码注入的方式实现 , 我没有细看 , 有兴趣的可以了解下 。
虚拟线程并不是万能的 , 虽然可以显著提高应用程序吞吐量 , 但也有前提:
并发任务的数量很高(超过几千个)
工作负载不受 CPU 限制 , 换句话说是I/O密集型的任务 。如果是计算密集型任务 , 拥有比处理器内核多得多的线程并不能提高吞吐量
举个例子 , 假设有这样一个场景 , 需要同时启动10000个任务做一些事情:
【一些杂想:Java老矣,尚能饭否?】// 创建一个虚拟线程的Executor , 该Executor每执行一个任务就会创建一个新的虚拟线程try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {IntStream.range(0, 10_000).forEach(i -> {executor.submit(() -> {doSomething();return i;});});}// executor.close() is called implicitly, and waits
如果()里执行的是某类I/O操作 , 那么使用虚拟线程是非常合适的 , 因为虚拟线程创建和切换的代价很低 , 底层对应的可能只需要几个OS线程 。如果没有虚拟线程 , 使用线程的话可能要这样写了:
如果()里执行的是某类计算任务 , 例如给一个大数组排序 , 那么虚拟线程还是平台线程都无济于事 。JEP中提到了很关键的一点就是:虚拟线程不是更快的线程—它们运行代码的速度并不比平台线程快 。它们的存在是为了提供scale(更高的吞吐量) , 而不是speed(更低的延迟) 。
虚拟线程的提案[51]目前还是状态 , 因此我们还无从知晓其最终形态 , 也许可以确定的几点:
// 直接创建一个虚拟线程Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));// 通过builder创建一个虚拟线程Thread virtualThread = Thread.builder().virtual().task(() -> {System.out.println("Fiber Thread: " + Thread.currentThread().getName());}).start();// 创建一个基于虚拟线程的ExecutorServiceExecutorService executor = Executors.newVirtualThreadExecutor()
值类型
在Java架构师Brian Goetz的演讲[53]中讲到 ,  的目标是" theof data in " 。他提到Java的一些设计在刚开始是完全OK的 , 但过去25年中硬件发生了很大变化:
Java是一门重指针("")的语言 , 除了基本类型 , 可以说“一切皆为对象” , 每个对象都有其对象标识符[55]( ) 。面向对象的内存布局中 , 对象标识符存在的目的是为了允许在不暴露对象结构的前提下 , 依然可以引用其属性与行为 , 是Java实现多态性、可变性、锁等一系列功能的基础 。尴尬的是 , 不管你需不需要什么多态、可变性、锁 , 对象标识符就在那里 , 也就是演讲中说的:Not allneed that! But allpay for it 。
Java通过对象标识符进行链式访问 , 与之相对的是集中访问模式 , 例如C/C++中的会将对象在内存中拍平 。两者的关键区别在于 , 链式访问需要读多次内存才能命中 , 而集中访问一次就可以将相关数据全部取出 。打个比方 , 类A中包含类B , 类B中包含类C , 从A->B->C , 链式访问在最坏情况下要读3次内存;而集中访问只需要读一次 。