你是不是被 ThreadLocal 的内存泄漏、父子线程传值麻烦、无法在线程池里正确传递、手动清理麻烦 这些坑折磨过?

Java 21+ 正式推出的 ScopedValue 就是 ThreadLocal 的终极替代品,官方钦定、零内存泄漏、天然支持虚拟线程、父子线程/线程池无缝传值、代码极简!

这篇教程从入门 → 实战 → 原理 → 对比 ThreadLocal,一次性讲透!


一、先搞懂:ScopedValue 是什么?

一句话总结
ScopedValue线程内、限定作用域的只读变量,只能在指定代码块内生效,出了作用域自动销毁,完全不用手动清理

核心特性:

  • 零内存泄漏(作用域结束自动释放)
  • 天然支持虚拟线程
  • 父子线程/线程池 自动安全传值
  • 只读不可修改(线程安全)
  • API 极简(比 ThreadLocal 少 80% 代码)
  • 性能更高

二、环境要求

  • JDK 21+(正式版,预览已结束)
  • Maven/Gradle 正常项目即可

三、极速入门:3 行代码学会 ScopedValue

1. 基础使用模板

public class ScopedValueDemo {
    // 1. 创建 ScopedValue 实例(全局单例)
    private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        // 2. 绑定值 + 限定作用域
        ScopedValue.where(USER_ID, "10086")
                   .run(() -> {
                       // 3. 在作用域内获取值
                       System.out.println("作用域内:" + USER_ID.get()); // 输出 10086
                       test(); // 调用方法也能获取
                   });

        // 作用域外:获取不到!
        try {
            System.out.println("作用域外:" + USER_ID.get());
        } catch (NoSuchElementException e) {
            System.out.println("作用域外:无值");
        }
    }

    private static void test() {
        System.out.println("方法内:" + USER_ID.get()); // 10086
    }
}

输出结果

作用域内:10086
方法内:10086
作用域外:无值

四、核心 API(就 4 个,记完直接用)

1. 创建实例

ScopedValue<String> SV = ScopedValue.newInstance();

2. 绑定值 + 执行代码

// 无返回值
ScopedValue.where(SV, "值").run(()->{});

// 有返回值
T result = ScopedValue.where(SV, "值").call(()->{ return "结果"; });

3. 获取值

SV.get(); // 没有值会抛异常
SV.isBound(); // 判断是否有值
SV.orElse("默认值"); // 安全获取

4. 多变量绑定(链式调用)

ScopedValue.where(USER_ID, "10086")
           .where(USER_NAME, "张三")
           .run(() -> {
               // 两个值都能获取
           });

五、最强特性:父子线程 / 线程池 自动传值

ThreadLocal 最大痛点:子线程/线程池拿不到父线程的值!
ScopedValue 天生解决!

示例:子线程自动继承值

public class ScopedValueThreadDemo {
    private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();

    public static void main(String[] args) throws InterruptedException {
        ScopedValue.where(TRACE_ID, "trace-123456")
                   .run(() -> {
                       System.out.println("主线程:" + TRACE_ID.get());

                       // 子线程 自动继承值!
                       Thread thread = new Thread(() -> {
                           System.out.println("子线程:" + TRACE_ID.get()); // 能拿到!
                       });
                       thread.start();
                       thread.join();
                   });
    }
}

输出

主线程:trace-123456
子线程:trace-123456

线程池也完美支持!

ExecutorService executor = Executors.newFixedThreadPool(3);

ScopedValue.where(TRACE_ID, "trace-xxx")
           .run(() -> {
               // 线程池中的任务 自动继承值
               executor.submit(() -> {
                   System.out.println("线程池:" + TRACE_ID.get());
               });
           });

六、实战场景:微服务全链路追踪(替代 MDC)

微服务里最常用 ThreadLocal 存 traceId,现在用 ScopedValue 更安全!

public class TraceUtil {
    // 全局唯一
    public static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();

    public static String getTraceId() {
        return TRACE_ID.orElse("default-trace");
    }
}

拦截器使用

// 请求入口
ScopedValue.where(TraceUtil.TRACE_ID, generateTraceId())
           .run(() -> {
               // 执行业务逻辑
               controller.service.dao();
           });

任何地方直接获取:

TraceUtil.getTraceId()

七、ScopedValue vs ThreadLocal(终极对比)

特性 ScopedValue ThreadLocal
内存泄漏 ❌ 无(自动释放) ✅ 极易泄漏
手动清理 ❌ 不需要 ✅ 必须 remove
父子线程传值 ✅ 天然支持 ❌ 不支持
线程池支持 ✅ 完美 ❌ 严重污染
虚拟线程 ✅ 官方推荐 ❌ 不推荐
可变性 ❌ 只读(安全) ✅ 可修改(不安全)
代码量 ✔ 极少 ❌ 繁琐
性能 ✅ 更高 ❌ 较低

八、为什么 ScopedValue 不会内存泄漏?

  • 作用域严格限定run()/call() 代码块执行完,值自动销毁
  • 不依附 Thread:不占用线程生命周期
  • 无强引用滞留:GC 可以直接回收
  • 只读不可改:无并发安全问题

九、最佳实践(企业级规范)

  1. 一个模块一个 ScopedValue 实例
  2. 统一放在工具类里(如 TraceUtil、UserContext)
  3. 作用域越小越好(只在需要的地方绑定)
  4. 优先使用 orElse() 避免空异常
  5. 微服务 traceId、登录用户、请求上下文 全部替换 ThreadLocal
  6. Java 21+ 新项目强制用 ScopedValue

十、一句话总结

ThreadLocal 可以彻底再见了!
ScopedValue = 安全 + 简洁 + 高性能 + 无泄漏 + 支持虚拟线程/线程池


总结

  1. ScopedValue 是 ThreadLocal 官方替代品,JDK21+ 正式可用
  2. API 极简:newInstance()where()run/call()get()
  3. 自动作用域管理,无内存泄漏、无需手动清理
  4. 父子线程/线程池 天然传值,解决 ThreadLocal 最大痛点
  5. 新项目强制使用,老项目逐步替换

更多推荐