Java虚拟线程能否为载体线程创建ThreadLocal?缓存方案探讨
JEP-425(虚拟线程)指出,应为每个应用任务创建新的虚拟线程,JVM可同时运行数百万个虚拟线程。该JEP提到:
虚拟线程像平台线程一样支持thread-local变量[...],因此可运行使用thread local的现有代码。
但同时也警告:
但由于虚拟线程数量可能极多,使用thread local需谨慎考量。
虚拟线程不应该被池化,作为短生命周期任务的载体,在虚拟线程中用ThreadLocal缓存创建成本高昂的非线程安全对象(比如DateFormat)几乎毫无意义——除非能将缓存绑定到虚拟线程的载体平台线程上。
比如在原生线程池场景下可行的代码:
static final ThreadLocal<DateFormat> CACHED = ThreadLocal.withInitial(DateFormat::getInstance);
在持续创建数百万虚拟线程时会失效,因此考虑改用非公共API实现:
static final ThreadLocal<DateFormat> CACHED = new jdk.internal.misc.CarrierThreadLocal();// CACHED.set(...)
关于载体线程的假设与限制
我对虚拟线程的理解是:它们是在平台线程(载体线程)上执行的逻辑阶段,等待时会卸载而非阻塞载体线程。基于此有两个假设:
- 除非代码发生阻塞,否则同一载体线程上的虚拟线程不会被抢占或调度到其他载体线程
- 如果缓存对象的操作不涉及阻塞,任务会全程在同一载体线程运行,此时将对象缓存到载体线程的ThreadLocal是安全的
但JEP-425明确说明:
载体线程的thread-local变量对虚拟线程不可用,反之亦然。
目前也未找到从虚拟线程获取载体线程,或直接操作载体线程ThreadLocal的公共API。
Scoped Values的替代方案
JEP-429(作用域值)是官方提出的ThreadLocal替代方案,尤其适合大量虚拟线程的场景,相比ThreadLocal有明显优势。但JEP-429也提到:
少数场景更适合thread-local变量,例如缓存java.text.DateFormat这类创建成本高的可变对象,为每个线程分配独立实例是实用方案。
不过在百万级虚拟线程的场景下,ThreadLocal会带来巨大的内存占用,显然不是理想方案。
核心问题
- 是否存在从虚拟线程为载体线程分配ThreadLocal的合法方法?
- 如果不存在,是否意味着虚拟线程场景下用ThreadLocal缓存对象的做法已过时,需要改用并发缓存等替代方案?
内容的提问来源于stack exchange,提问作者Martin Andersson

