概括:
选择平台线程而非虚拟线程的原因不在于任务运行时间长短,而是在于(a)任务是否涉及少量阻塞操作,或(b)任务中是否有长时间运行且使用了 synchronized
关键字的代码。否则,应优先考虑使用虚拟线程。
详细说明:
我认为你可能对此问题过度思考了,其实情况并没有那么复杂。
在Java 21及以后版本中...
将你想要执行的任务定义为 Runnable
或 Callable
对象。
如果你不在乎任务是否并发执行,可以通过 execute
方法将其传递给 Executor
实例。
如果你想确保任务并发执行,可以将任务通过 submit
或 invoke...
方法传递给 ExecutorService
。
如果该任务涉及到阻塞操作,应该使用一个为每个任务分配新虚拟线程的executor服务。“新鲜”意味着新的、干净的、堆栈极小,并且没有预先存在的 ThreadLocal
对象。“阻塞”指的是等待某个操作完成,使得线程无法进行进一步的工作,基本包括日志记录、文件读写、数据库调用和网络通信等I/O操作。虚拟线程极其“便宜”,这意味着创建快速、内存效率高、CPU效率高。
换句话说,虚拟线程就像面巾纸:需要时取出一张新的来用,用完后就可以丢弃。
如果你的任务涉及到了长时间运行并被 synchronized
标记的代码,请将 synchronized
替换为 ReentrantLock
使用,或者使用平台线程来运行这样的任务,接下来将进行描述。
如果任务(a)不涉及阻塞(如视频编码这样的CPU密集型任务),或(b)调用了本地代码(JNI或JEP 454),则不应使用虚拟线程。对于这类任务,应提交到由平台线程支持的executor服务中。若担心对计算机造成过重负担,可使用由有限数量线程池支持的executor服务。
平台线程是“昂贵”的,因此在满足上述条件的情况下,应优先选择虚拟线程。虚拟线程中的任务可能运行时间短暂或较长,甚至可能会持续应用程序的整个生命周期。
在使用线程池时,务必注意清理 ThreadLocal
对象,以避免后续任务在同一个线程中意外使用这些对象。
始终在应用程序结束前关闭executor服务,否则后台线程可能会像僵尸一样无限期地运行下去。你可以使用try-with-resources语法自动关闭,或者按照 ExecutorService
Javadoc 中提供的示例代码来进行关闭处理。
顺便提一下,虚拟线程实际上是将你的任务在一个由JVM自动管理的平台线程池中的某个线程上运行。
欲了解更多相关信息,请阅读上述链接的JEP文档,并观看Ron Pressler、Alan Bateman和José Paumard的相关演讲。