陷阱:固定与 ThreadLocals
避免阻塞载体线程
陷阱:固定与 ThreadLocals 是 CoddyKit 上的免费 Java Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。
可能出现的问题
虚拟线程功能强大,但有两个陷阱可能悄然削弱其可扩展性:线程固定和过度使用 ThreadLocals。
本课将解释这两个问题以及避免它们的方法。
什么是线程固定
当虚拟线程阻塞时,如果无法从其载体线程上卸载,就会发生线程固定。载体 OS 线程会一直卡住,从而抵消虚拟线程带来的可扩展性优势。
当过多载体线程被固定时,吞吐量会崩溃。
原因:同步代码中的阻塞
最常见的原因是在 synchronized 块或方法内部发生阻塞。持有监视器并随后阻塞时,虚拟线程会一直固定在其载体线程上。
注意:在 JDK 24+(JEP 491)中,这一限制已基本消除,但在 Java 21 中仍然是一个实际问题。
应避免的线程固定示例
这种模式在 Java 21 中可能导致线程固定:持有监视器时发生阻塞。代码仍能正确运行,但无法实现良好的扩展。
public class Main {
static final Object lock = new Object();
static void risky() {
synchronized (lock) {
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t = Thread.ofVirtual().start(Main::risky);
t.join();
System.out.println("Done (but this blocked inside synchronized)");
}
}修复方法:ReentrantLock
当临界区可能阻塞时,请用 ReentrantLock 替换 synchronized。锁接口适合虚拟线程,并允许线程卸载。
import java.util.concurrent.locks.ReentrantLock;
public class Main {
static final ReentrantLock lock = new ReentrantLock();
static void safe() {
lock.lock();
try {
try { Thread.sleep(10); } catch (InterruptedException e) {}
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t = Thread.ofVirtual().start(Main::safe);
t.join();
System.out.println("Done without pinning");
}
}诊断线程固定
您可以在启动程序时加入以下参数,让 JVM 在发生线程固定时打印堆栈跟踪:
-Djdk.tracePinnedThreads=full:打印完整堆栈跟踪-Djdk.tracePinnedThreads=short:打印单行信息
这有助于您定位有问题的 synchronized 代码段。
ThreadLocals 的成本变高
ThreadLocal 用于存储每个线程的状态。对于少量平台线程来说,这没有问题。但如果有数百万个虚拟线程,每个线程都携带自己的副本,内存占用会急剧膨胀。
请检查您使用的库:缓存、格式化和上下文对象中经常隐藏着 ThreadLocals。
ThreadLocal 仍然有效
ThreadLocal 并非禁用,只是应当谨慎使用。每个虚拟线程都会按预期获得自己的值。
public class Main {
static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t = Thread.ofVirtual().start(() -> {
CONTEXT.set("request-42");
System.out.println("Context: " + CONTEXT.get());
CONTEXT.remove();
});
t.join();
}
}优先使用 ScopedValue
Java 引入了 ScopedValue,作为一种更轻量的替代方案,用于在有限的生命周期内共享不可变数据。它避免了 ThreadLocal 按线程存储可变数据的成本,并且符合结构化并发模型。
如果您只需沿调用链向下传递只读上下文,请改用它。
不要放入线程池
另一个不易察觉的陷阱,是把虚拟线程视为稀缺资源并将它们放入线程池。这会重新引入竞争,以及任务之间的 ThreadLocal 泄漏。
始终为每个任务创建一个虚拟线程,然后让它终止。
规模化检查清单
在将虚拟线程投入生产之前:
- 用
ReentrantLock替换会阻塞的synchronized - 在测试中启用
jdk.tracePinnedThreads - 尽量减少使用
ThreadLocal;优先使用ScopedValue - 绝不要将虚拟线程放入线程池
快速检验
找出会阻塞的同步代码块的安全替代方案。
回顾
您已经了解了主要陷阱:
- 线程固定:在
synchronized内部阻塞会占用载体线程;使用ReentrantLock修复 - 使用
-Djdk.tracePinnedThreads进行诊断 - ThreadLocal 的开销会随线程数量增长;优先使用
ScopedValue - 绝不要将虚拟线程放入线程池
虚拟线程课程到此完成。
常见问题解答
「陷阱:固定与 ThreadLocals」课时是免费的吗?
是的 — 「陷阱:固定与 ThreadLocals」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。
「陷阱:固定与 ThreadLocals」这节课中我会学到什么?
避免阻塞载体线程 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Java Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「陷阱:固定与 ThreadLocals」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Java Academy 课中编写并运行代码吗?
能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。