0Pricing
Java Academy · 课时

陷阱:固定与 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 什么是虚拟线程
  2. 创建虚拟线程
  3. 平台线程与虚拟线程
  4. 陷阱:固定与 ThreadLocals
← 返回 Java Academy