ThreadLocal 深度解析(下)

92 阅读29分钟

接着ThreadLocal 深度解析(上)的内容。

第四章:ThreadLocal 生产实践

1. 使用规范

1.1 声明规范

ThreadLocal的声明方式直接影响其生命周期和内存使用。错误的声明方式是导致内存泄漏的常见原因之一。

核心原则:ThreadLocal应该声明为static final类型。这确保了整个应用中只有一个ThreadLocal实例,避免了重复创建带来的内存问题。

/**
 * 推荐:static final修饰
 * 原因:确保ThreadLocal实例唯一,避免重复创建
 */
public class UserContext {
    private static final ThreadLocal<User> HOLDER = new ThreadLocal<>();
}

/**
 * 避免:非静态声明
 * 问题:每次创建对象都会new一个ThreadLocal,导致内存泄漏
 */
public class BadExample {
    private ThreadLocal<User> holder = new ThreadLocal<>();  // 危险!
}

/**
 * 避免:方法内声明
 * 问题:每次调用都创建新实例,之前的值无法访问也无法清理
 */
public void badMethod() {
    ThreadLocal<String> local = new ThreadLocal<>();  // 危险!
    local.set("value");
    // local出了方法作用域,但Entry仍在ThreadLocalMap中
}

1.2 初始化规范

/**
 * 推荐:使用withInitial提供初始值
 */
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

private static final ThreadLocal<List<String>> BUFFER = 
    ThreadLocal.withInitial(ArrayList::new);

/**
 * 可选:重写initialValue方法
 */
private static final ThreadLocal<StringBuilder> STRING_BUILDER = 
    new ThreadLocal<StringBuilder>() {
        @Override
        protected StringBuilder initialValue() {
            return new StringBuilder(256);  // 预分配容量
        }
    };

/**
 *  注意:初始值中不要引用外部可变状态
 */
//  错误:引用外部对象
private final Config config;
private static final ThreadLocal<Processor> PROCESSOR = 
    ThreadLocal.withInitial(() -> new Processor(config));  // 编译错误或运行时问题

//  正确:使用静态配置或无参构造
private static final ThreadLocal<Processor> PROCESSOR = 
    ThreadLocal.withInitial(Processor::new);

1.3 使用与清理规范

这是ThreadLocal使用中最重要的规范,也是最容易被忽视的。无论业务逻辑多么复杂,无论代码路径有多少分支,都必须确保remove()方法最终被调用。

实现这一点的唯一可靠方式是使用try-finally结构。下面展示三种常用的模式,复杂度递增:

/**
 *  标准模式:try-finally确保清理
 * 适用于:简单场景,不涉及嵌套调用
 */
public void standardUsage() {
    try {
        USER_CONTEXT.set(currentUser);
        // 执行业务逻辑
        processRequest();
    } finally {
        USER_CONTEXT.remove();  // 必须清理
    }
}

/**
 *  增强模式:支持嵌套调用
 */
public void nestedSafeUsage() {
    User previous = USER_CONTEXT.get();  // 保存之前的值
    try {
        USER_CONTEXT.set(currentUser);
        processRequest();
    } finally {
        if (previous != null) {
            USER_CONTEXT.set(previous);  // 恢复之前的值
        } else {
            USER_CONTEXT.remove();
        }
    }
}

/**
 *  工具方法:自动管理生命周期
 */
public static <T, R> R withContext(ThreadLocal<T> threadLocal, T value, 
                                    Supplier<R> action) {
    T previous = threadLocal.get();
    try {
        threadLocal.set(value);
        return action.get();
    } finally {
        if (previous != null) {
            threadLocal.set(previous);
        } else {
            threadLocal.remove();
        }
    }
}

// 使用
String result = withContext(USER_CONTEXT, user, () -> {
    return businessService.process();
});

2. 常见陷阱与解决方案

理论知识固然重要,但真正让开发者栽跟头的往往是那些"看起来没问题"的代码。

2.1 陷阱一:线程池中忘记清理

这是ThreadLocal最常见、危害最大的陷阱。几乎所有使用ThreadLocal的生产事故都与此有关。

问题描述:线程池中的线程会被复用,如果不清理ThreadLocal,值会残留到下一个任务。这可能导致数据错乱、权限泄露等严重问题。

/**
 *  问题代码
 */
public class ThreadPoolTrap {
    
    private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
    private static final ExecutorService executor = Executors.newFixedThreadPool(2);
    
    public void process(String requestId) {
        executor.submit(() -> {
            REQUEST_ID.set(requestId);
            doSomething();
            // 忘记remove,下一个任务可能读到这个值
        });
    }
}

/**
 *  解决方案
 */
public class ThreadPoolFixed {
    
    private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
    
    public void process(String requestId) {
        executor.submit(() -> {
            try {
                REQUEST_ID.set(requestId);
                doSomething();
            } finally {
                REQUEST_ID.remove();  // 必须清理
            }
        });
    }
}

2.2 陷阱二:数据污染(脏读)

问题描述:由于线程复用,读取到上一个任务遗留的值。

/**
 * 真实案例:用户权限错乱
 */
public class DataPollutionCase {
    
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
    
    // 场景:用户A的请求处理完后没有清理
    // 结果:用户B的请求复用了同一个线程,读取到用户A的ID
    
    public void handleRequest(HttpServletRequest request) {
        //  如果没有重新set,可能读到上一个用户的ID
        String userId = USER_ID.get();  // 可能是用户A的ID!
        
        // 权限检查...基于错误的userId
    }
    
    /**
     *  正确做法:每次请求都重新设置
     */
    public void handleRequestFixed(HttpServletRequest request) {
        try {
            String userId = extractUserId(request);
            USER_ID.set(userId);  // 明确设置当前请求的userId
            
            // 业务处理
            processRequest();
            
        } finally {
            USER_ID.remove();
        }
    }
}

2.3 陷阱三:父子线程值传递失败

问题描述:子线程无法获取父线程的ThreadLocal值。

/**
 *  问题代码
 */
public class ParentChildTrap {
    
    private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
    
    public void asyncProcess() {
        TRACE_ID.set("trace-001");
        
        new Thread(() -> {
            // 子线程获取不到父线程的值
            String traceId = TRACE_ID.get();  // null!
            System.out.println("子线程TraceId: " + traceId);
        }).start();
    }
}

/**
 *  解决方案1:使用InheritableThreadLocal
 */
public class ParentChildFixed1 {
    
    // InheritableThreadLocal可以传递给子线程
    private static final ThreadLocal<String> TRACE_ID = new InheritableThreadLocal<>();
    
    public void asyncProcess() {
        TRACE_ID.set("trace-001");
        
        new Thread(() -> {
            String traceId = TRACE_ID.get();  // "trace-001"
            System.out.println("子线程TraceId: " + traceId);
        }).start();
    }
}

/**
 *  解决方案2:手动传递值(更推荐)
 */
public class ParentChildFixed2 {
    
    public void asyncProcess() {
        String traceId = "trace-001";
        
        // 通过参数或闭包传递
        new Thread(() -> {
            // 直接使用traceId变量
            System.out.println("子线程TraceId: " + traceId);
        }).start();
    }
}

2.4 陷阱四:InheritableThreadLocal + 线程池

很多开发者在遇到陷阱三后,会尝试使用InheritableThreadLocal来解决问题。这确实能解决普通子线程的值传递问题,但在线程池场景下又会遇到新的陷阱。

问题描述InheritableThreadLocal只在线程创建时复制父线程的值。线程池中的线程是复用的,不会每次都重新创建,因此后续任务会读取到第一次创建时继承的旧值。

/**
 * 问题代码
 */
public class ITLThreadPoolTrap {
    
    private static final InheritableThreadLocal<String> CONTEXT = 
        new InheritableThreadLocal<>();
    private static final ExecutorService executor = Executors.newFixedThreadPool(1);
    
    public static void main(String[] args) throws Exception {
        // 第一次:设置值并提交任务
        CONTEXT.set("request-1");
        executor.submit(() -> {
            System.out.println("任务1: " + CONTEXT.get());  // "request-1"
        }).get();
        
        // 第二次:更新值并提交任务
        CONTEXT.set("request-2");
        executor.submit(() -> {
            //  仍然是"request-1"!因为线程复用,不会重新继承
            System.out.println("任务2: " + CONTEXT.get());  // "request-1"
        }).get();
    }
}

/**
 *  解决方案:使用TransmittableThreadLocal(阿里开源)
 * 详见第05章
 */

2.5 陷阱五:异常导致finally不执行

我们一直强调使用try-finally来确保remove()被调用,但你是否知道,finally并不是100%可靠的?在某些极端情况下,finally块根本不会执行。

问题描述:虽然罕见,但以下情况会导致finally不执行,从而造成ThreadLocal无法清理:

/**
 * finally不执行的情况:
 * 1. System.exit()
 * 2. Runtime.halt()
 * 3. 守护线程被强制终止
 * 4. 无限循环/死锁在try块中
 * 5. JVM崩溃
 */
public class FinallyTrap {
    
    public void riskyMethod() {
        try {
            CONTEXT.set(value);
            
            // 如果这里调用System.exit(0),finally不会执行
            if (shouldExit) {
                System.exit(0);
            }
            
            processRequest();
            
        } finally {
            CONTEXT.remove();  // 可能不执行
        }
    }
}

/**
 *  解决方案:使用ShutdownHook清理
 */
public class FinallyFixed {
    
    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            // 清理静态ThreadLocal
            CONTEXT.remove();
        }));
    }
}

3. 生产级工具类

既然ThreadLocal使用起来这么容易出错,为什么不封装一些工具类来简化使用呢?本节提供三个生产环境验证过的工具类,你可以直接在项目中使用或根据需要进行修改。

这些工具类的设计目标是:让正确使用ThreadLocal变得简单,让错误使用变得困难。

3.1 线程上下文管理器

当一个应用中有多个ThreadLocal时,逐个清理非常繁琐且容易遗漏。ThreadContextManager提供了统一管理多个ThreadLocal的能力:

/**
 * 通用线程上下文管理器
 * 支持多个ThreadLocal的统一管理
 */
public class ThreadContextManager {
    
    private static final List<ThreadLocal<?>> MANAGED_LOCALS = new ArrayList<>();
    
    /**
     * 注册需要管理的ThreadLocal
     */
    public static void register(ThreadLocal<?> threadLocal) {
        MANAGED_LOCALS.add(threadLocal);
    }
    
    /**
     * 清理所有已注册的ThreadLocal
     */
    public static void clearAll() {
        for (ThreadLocal<?> local : MANAGED_LOCALS) {
            local.remove();
        }
    }
    
    /**
     * 获取当前上下文快照
     */
    public static Map<ThreadLocal<?>, Object> snapshot() {
        Map<ThreadLocal<?>, Object> snapshot = new HashMap<>();
        for (ThreadLocal<?> local : MANAGED_LOCALS) {
            Object value = local.get();
            if (value != null) {
                snapshot.put(local, value);
            }
        }
        return snapshot;
    }
    
    /**
     * 恢复上下文
     */
    @SuppressWarnings("unchecked")
    public static void restore(Map<ThreadLocal<?>, Object> snapshot) {
        clearAll();
        for (Map.Entry<ThreadLocal<?>, Object> entry : snapshot.entrySet()) {
            ((ThreadLocal<Object>) entry.getKey()).set(entry.getValue());
        }
    }
}

3.2 安全的ThreadLocal包装器

如果你希望在执行完操作后自动清理ThreadLocal,而不用每次都写try-finally,可以使用这个包装器。它还支持在清理时执行自定义的资源释放逻辑(如关闭连接):

/**
 * 自动清理的ThreadLocal包装器
 */
public class AutoCleanThreadLocal<T> extends ThreadLocal<T> {
    
    private final Supplier<T> initializer;
    private final Consumer<T> cleaner;
    
    public AutoCleanThreadLocal(Supplier<T> initializer) {
        this(initializer, null);
    }
    
    public AutoCleanThreadLocal(Supplier<T> initializer, Consumer<T> cleaner) {
        this.initializer = initializer;
        this.cleaner = cleaner;
    }
    
    @Override
    protected T initialValue() {
        return initializer != null ? initializer.get() : null;
    }
    
    @Override
    public void remove() {
        T value = get();
        if (cleaner != null && value != null) {
            cleaner.accept(value);  // 执行清理逻辑
        }
        super.remove();
    }
    
    /**
     * 执行操作并自动清理
     */
    public <R> R executeAndClean(Function<T, R> action) {
        try {
            return action.apply(get());
        } finally {
            remove();
        }
    }
}

// 使用示例
AutoCleanThreadLocal<Connection> connectionHolder = new AutoCleanThreadLocal<>(
    () -> dataSource.getConnection(),
    conn -> {
        try { conn.close(); } catch (SQLException e) { /* log */ }
    }
);

connectionHolder.executeAndClean(conn -> {
    // 使用连接
    return conn.prepareStatement(sql).executeQuery();
});

3.3 线程池任务包装器

在线程池场景中,我们经常需要将父线程的ThreadLocal值传递给工作线程。这个包装器可以自动完成上下文的捕获、传递和清理工作,是一个简化版的TransmittableThreadLocal:

/**
 * 自动传递和清理ThreadLocal的任务包装器
 */
public class ContextAwareRunnable implements Runnable {
    
    private final Runnable delegate;
    private final Map<ThreadLocal<?>, Object> context;
    
    public ContextAwareRunnable(Runnable delegate) {
        this.delegate = delegate;
        this.context = ThreadContextManager.snapshot();  // 捕获当前上下文
    }
    
    @Override
    public void run() {
        Map<ThreadLocal<?>, Object> previous = ThreadContextManager.snapshot();
        try {
            ThreadContextManager.restore(context);  // 恢复上下文
            delegate.run();
        } finally {
            ThreadContextManager.restore(previous);  // 恢复原始上下文
        }
    }
    
    /**
     * 便捷包装方法
     */
    public static Runnable wrap(Runnable runnable) {
        return new ContextAwareRunnable(runnable);
    }
    
    public static <T> Callable<T> wrap(Callable<T> callable) {
        return new ContextAwareCallable<>(callable);
    }
}

// 使用
executor.submit(ContextAwareRunnable.wrap(() -> {
    // 可以访问父线程的ThreadLocal值
    String traceId = TRACE_CONTEXT.get();
}));

4. 框架集成最佳实践

在实际项目中,我们通常会使用Spring等框架。本节展示如何在主流框架中正确使用ThreadLocal,这些都是经过生产验证的模式。

4.1 Spring MVC集成

在Spring MVC中,最常见的ThreadLocal使用场景是在拦截器中设置请求上下文。关键点是:在preHandle中设置,在afterCompletion中清理(注意不是postHandle,因为postHandle在异常时不会执行):

/**
 * Spring拦截器中使用ThreadLocal
 */
@Component
public class RequestContextInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) {
        // 设置请求上下文
        RequestContext context = new RequestContext();
        context.setRequestId(generateRequestId());
        context.setUserId(extractUserId(request));
        context.setStartTime(System.currentTimeMillis());
        
        RequestContextHolder.set(context);
        
        // 设置MDC用于日志
        MDC.put("requestId", context.getRequestId());
        MDC.put("userId", context.getUserId());
        
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, Exception ex) {
        try {
            // 记录请求耗时
            RequestContext context = RequestContextHolder.get();
            if (context != null) {
                long duration = System.currentTimeMillis() - context.getStartTime();
                log.info("请求完成,耗时: {}ms", duration);
            }
        } finally {
            // 清理所有上下文
            RequestContextHolder.clear();
            MDC.clear();
        }
    }
}

/**
 * 请求上下文持有者
 */
public class RequestContextHolder {
    
    private static final ThreadLocal<RequestContext> HOLDER = new ThreadLocal<>();
    
    public static void set(RequestContext context) {
        HOLDER.set(context);
    }
    
    public static RequestContext get() {
        return HOLDER.get();
    }
    
    public static void clear() {
        HOLDER.remove();
    }
    
    public static String getRequestId() {
        RequestContext ctx = get();
        return ctx != null ? ctx.getRequestId() : null;
    }
}

4.2 MyBatis事务集成

/**
 * 简化的事务管理器(演示用)
 */
public class SimpleTransactionManager {
    
    private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<>();
    private static final ThreadLocal<Boolean> TRANSACTION_ACTIVE = new ThreadLocal<>();
    
    private final DataSource dataSource;
    
    public void beginTransaction() throws SQLException {
        if (Boolean.TRUE.equals(TRANSACTION_ACTIVE.get())) {
            throw new IllegalStateException("事务已经开启");
        }
        
        Connection conn = dataSource.getConnection();
        conn.setAutoCommit(false);
        CONNECTION_HOLDER.set(conn);
        TRANSACTION_ACTIVE.set(true);
    }
    
    public void commit() throws SQLException {
        Connection conn = CONNECTION_HOLDER.get();
        if (conn != null) {
            conn.commit();
        }
    }
    
    public void rollback() {
        Connection conn = CONNECTION_HOLDER.get();
        if (conn != null) {
            try {
                conn.rollback();
            } catch (SQLException e) {
                log.error("回滚失败", e);
            }
        }
    }
    
    public void close() {
        try {
            Connection conn = CONNECTION_HOLDER.get();
            if (conn != null) {
                conn.setAutoCommit(true);
                conn.close();
            }
        } catch (SQLException e) {
            log.error("关闭连接失败", e);
        } finally {
            CONNECTION_HOLDER.remove();
            TRANSACTION_ACTIVE.remove();
        }
    }
    
    public Connection getConnection() {
        return CONNECTION_HOLDER.get();
    }
}

4.3 日志链路追踪集成

链路追踪是微服务架构中的标配功能。通过ThreadLocal配合日志框架的MDC,可以实现自动在每条日志中输出TraceId,而无需修改任何业务代码。这是一个非常优雅的设计模式:

/**
 * 链路追踪上下文
 */
public class TraceContext {
    
    private static final ThreadLocal<TraceInfo> HOLDER = 
        ThreadLocal.withInitial(TraceInfo::new);
    
    public static TraceInfo get() {
        return HOLDER.get();
    }
    
    public static void set(TraceInfo info) {
        HOLDER.set(info);
        // 同步到MDC
        MDC.put("traceId", info.getTraceId());
        MDC.put("spanId", info.getSpanId());
    }
    
    public static void clear() {
        HOLDER.remove();
        MDC.remove("traceId");
        MDC.remove("spanId");
    }
    
    /**
     * 创建新的追踪上下文
     */
    public static TraceInfo newTrace() {
        TraceInfo info = new TraceInfo();
        info.setTraceId(generateTraceId());
        info.setSpanId(generateSpanId());
        set(info);
        return info;
    }
    
    /**
     * 从请求头中恢复追踪上下文
     */
    public static TraceInfo fromHeaders(HttpServletRequest request) {
        String traceId = request.getHeader("X-Trace-Id");
        String parentSpanId = request.getHeader("X-Span-Id");
        
        TraceInfo info = new TraceInfo();
        info.setTraceId(traceId != null ? traceId : generateTraceId());
        info.setParentSpanId(parentSpanId);
        info.setSpanId(generateSpanId());
        
        set(info);
        return info;
    }
}

@Data
public class TraceInfo {
    private String traceId;
    private String spanId;
    private String parentSpanId;
}

5. 性能优化建议

虽然ThreadLocal的性能已经很好(get/set都是O(1)操作),但不正确的使用方式仍可能带来性能问题。本节分享几个常见的性能陷阱和优化建议。

5.0 性能基准测试数据

JMH基准测试代码

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class ThreadLocalBenchmark {
    
    private static final ThreadLocal<String> TL = ThreadLocal.withInitial(() -> "value");
    private static final ThreadLocal<String> TL_EMPTY = new ThreadLocal<>();
    
    @Setup(Level.Iteration)
    public void setup() {
        TL.set("benchmark-value");
    }
    
    @Benchmark
    public String testGet() {
        return TL.get();
    }
    
    @Benchmark
    public void testSet() {
        TL.set("new-value");
    }
    
    @Benchmark
    public void testRemove() {
        TL.remove();
    }
    
    @Benchmark
    public void testSetThenRemove() {
        TL.set("value");
        TL.remove();
    }
    
    @TearDown(Level.Iteration)
    public void tearDown() {
        TL.remove();
    }
}

5.1 避免频繁创建ThreadLocal

这是最常见的性能问题。每次创建新的ThreadLocal实例,都会在ThreadLocalMap中新增一个Entry,不仅浪费内存,还可能触发扩容和清理操作。

/**
 *  性能问题:每次调用都创建ThreadLocal
 */
public String formatDate(Date date) {
    ThreadLocal<SimpleDateFormat> local = new ThreadLocal<>();  // 每次创建
    local.set(new SimpleDateFormat("yyyy-MM-dd"));
    return local.get().format(date);
}

/**
 *  优化:静态复用
 */
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public String formatDate(Date date) {
    return DATE_FORMAT.get().format(date);
}

5.2 控制ThreadLocal数量

每个ThreadLocal实例都会在ThreadLocalMap中占用一个槽位。如果一个类中定义了大量的ThreadLocal,会增加哈希冲突的概率,降低访问效率。更好的做法是将多个相关的值封装到一个对象中:

/**
 *  问题:过多的ThreadLocal
 */
public class TooManyThreadLocals {
    private static final ThreadLocal<String> FIELD1 = new ThreadLocal<>();
    private static final ThreadLocal<String> FIELD2 = new ThreadLocal<>();
    private static final ThreadLocal<String> FIELD3 = new ThreadLocal<>();
    // ... 更多字段
}

/**
 *  优化:使用对象封装
 */
public class OptimizedContext {
    private static final ThreadLocal<ContextData> CONTEXT = new ThreadLocal<>();
    
    @Data
    public static class ContextData {
        private String field1;
        private String field2;
        private String field3;
    }
}

5.3 延迟初始化大对象

使用withInitial()虽然方便,但对于大对象(如大数组、复杂对象)来说,可能造成不必要的内存浪费。如果不是每个线程都需要这个对象,应该采用延迟初始化的方式:

/**
 *  问题:不需要时也创建大对象
 */
private static final ThreadLocal<byte[]> BUFFER = 
    ThreadLocal.withInitial(() -> new byte[1024 * 1024]);  // 1MB

/**
 *  优化:按需创建
 */
private static final ThreadLocal<byte[]> BUFFER = new ThreadLocal<>();

public byte[] getBuffer() {
    byte[] buffer = BUFFER.get();
    if (buffer == null) {
        buffer = new byte[1024 * 1024];
        BUFFER.set(buffer);
    }
    return buffer;
}

第五章:ThreadLocal 进阶应用

1. InheritableThreadLocal详解

掌握了ThreadLocal的基本用法后,你可能会遇到一个新的问题:如何让子线程也能访问父线程的ThreadLocal值?这在异步编程中是一个常见的需求,比如在子线程中也需要访问用户上下文、TraceId等信息。

JDK提供了InheritableThreadLocal来解决这个问题,但它也有自己的局限性。本节将详细介绍它的原理、用法和注意事项。

1.1 为什么需要InheritableThreadLocal

先来看一个问题:普通ThreadLocal的值无法传递给子线程。这在很多场景下会造成困扰:

public class ThreadLocalInheritanceProblem {
    
    private static final ThreadLocal<String> NORMAL = new ThreadLocal<>();
    
    public static void main(String[] args) throws InterruptedException {
        NORMAL.set("父线程的值");
        
        Thread child = new Thread(() -> {
            // 子线程无法获取父线程的值
            System.out.println("子线程获取: " + NORMAL.get());  // null
        });
        
        child.start();
        child.join();
    }
}

InheritableThreadLocal解决了这个问题:

public class InheritableThreadLocalDemo {
    
    private static final InheritableThreadLocal<String> INHERITABLE = 
        new InheritableThreadLocal<>();
    
    public static void main(String[] args) throws InterruptedException {
        INHERITABLE.set("父线程的值");
        
        Thread child = new Thread(() -> {
            // 子线程可以获取父线程的值
            System.out.println("子线程获取: " + INHERITABLE.get());  // "父线程的值"
        });
        
        child.start();
        child.join();
    }
}

1.2 源码分析

InheritableThreadLocal继承自ThreadLocal,重写了三个方法:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    /**
     * 子线程创建时,如何处理父线程的值
     * 默认直接返回父线程的值(浅拷贝)
     * 可以重写实现深拷贝
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }
    
    /**
     * 使用inheritableThreadLocals而非threadLocals
     */
    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
    }
    
    /**
     * 创建时使用inheritableThreadLocals
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

1.3 继承时机:Thread构造函数

值的传递发生在Thread的构造函数中:

// Thread.java 构造函数(简化)
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    Thread parent = currentThread();
    
    // 如果父线程有inheritableThreadLocals,则复制给子线程
    if (parent.inheritableThreadLocals != null) {
        this.inheritableThreadLocals = 
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
}
sequenceDiagram
    participant Parent as 父线程
    participant Thread as Thread构造
    participant Child as 子线程
    
    Parent->>Parent: ITL.set("value")
    Parent->>Thread: new Thread()
    Thread->>Thread: init()
    Thread->>Thread: 检查parent.inheritableThreadLocals
    Thread->>Thread: createInheritedMap(复制)
    Thread->>Child: 子线程拥有副本
    Child->>Child: ITL.get() = "value"

1.4 深拷贝 vs 浅拷贝

默认的childValue()返回原对象引用(浅拷贝),父子线程共享同一对象:

/**
 * 浅拷贝问题演示
 */
public class ShallowCopyProblem {
    
    private static final InheritableThreadLocal<List<String>> LIST = 
        new InheritableThreadLocal<>();
    
    public static void main(String[] args) throws InterruptedException {
        List<String> parentList = new ArrayList<>();
        parentList.add("parent");
        LIST.set(parentList);
        
        Thread child = new Thread(() -> {
            List<String> childList = LIST.get();
            childList.add("child");  // 修改的是同一个对象!
        });
        
        child.start();
        child.join();
        
        // 父线程的List也被修改了
        System.out.println(LIST.get());  // [parent, child]
    }
}

/**
 * 深拷贝解决方案(简单对象)
 */
public class DeepCopySolution {
    
    private static final InheritableThreadLocal<List<String>> LIST = 
        new InheritableThreadLocal<List<String>>() {
            @Override
            protected List<String> childValue(List<String> parentValue) {
                // 创建新的ArrayList,实现深拷贝
                return parentValue != null ? new ArrayList<>(parentValue) : null;
            }
        };
}

注意:上面的new ArrayList<>(parentValue)只是浅拷贝List容器。如果List中的元素是可变对象,子线程修改元素内部状态仍会影响父线程!

递归深拷贝解决方案(复杂对象):

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 使用Jackson实现真正的递归深拷贝
 */
public class TrueDeepCopySolution {
    
    private static final ObjectMapper MAPPER = new ObjectMapper();
    
    private static final InheritableThreadLocal<UserContext> CONTEXT = 
        new InheritableThreadLocal<UserContext>() {
            @Override
            protected UserContext childValue(UserContext parentValue) {
                if (parentValue == null) return null;
                try {
                    // 序列化再反序列化,实现完全深拷贝
                    String json = MAPPER.writeValueAsString(parentValue);
                    return MAPPER.readValue(json, UserContext.class);
                } catch (Exception e) {
                    throw new RuntimeException("深拷贝失败", e);
                }
            }
        };
    
    @Data
    public static class UserContext implements Serializable {
        private String userId;
        private List<String> roles;      // 可变集合
        private Map<String, Object> attrs; // 可变Map
    }
}

/**
 * 或使用Cloneable接口(需要对象支持clone)
 */
public class CloneableDeepCopy {
    
    private static final InheritableThreadLocal<CloneableContext> CONTEXT = 
        new InheritableThreadLocal<CloneableContext>() {
            @Override
            protected CloneableContext childValue(CloneableContext parentValue) {
                return parentValue != null ? parentValue.deepClone() : null;
            }
        };
}

1.5 InheritableThreadLocal的局限性

16_InheritableThreadLocal局限性.drawio.png

核心问题:InheritableThreadLocal只在线程创建时复制,线程池复用线程时不会更新。

1.6 InheritableThreadLocal与虚拟线程

重要警告:在JDK 21+的虚拟线程场景下,不推荐使用InheritableThreadLocal

原因分析

问题说明
内存开销每个虚拟线程都会复制一份父线程的inheritableThreadLocals
复制时机虚拟线程创建时复制,但虚拟线程可能在不同载体线程上执行
官方不推荐JDK官方文档明确指出虚拟线程不适合使用ThreadLocal家族

虚拟线程场景的替代方案

//  不推荐:虚拟线程 + InheritableThreadLocal
private static final InheritableThreadLocal<String> ITL = new InheritableThreadLocal<>();

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    ITL.set("parent-value");
    executor.submit(() -> {
        // 虽然能获取到值,但每个虚拟线程都有一份拷贝
        // 百万虚拟线程 = 百万份拷贝 = 内存爆炸
        System.out.println(ITL.get());
    });
}

//  推荐:使用ScopedValue(JDK 21+)
private static final ScopedValue<String> SCOPED = ScopedValue.newInstance();

ScopedValue.where(SCOPED, "parent-value").run(() -> {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        scope.fork(() -> {
            // ScopedValue天然支持结构化并发,无额外内存开销
            System.out.println(SCOPED.get());
            return null;
        });
        scope.join();
    }
});

//  或者:显式参数传递
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    String contextValue = "parent-value";  // 捕获到lambda闭包
    executor.submit(() -> {
        // 通过闭包传递,简单可靠
        System.out.println(contextValue);
    });
}

InheritableThreadLocal使用建议

场景是否推荐替代方案
普通new Thread()可用-
传统线程池不推荐TTL
ForkJoinPool谨慎显式传参
虚拟线程强烈不推荐ScopedValue / 显式传参

2. TransmittableThreadLocal

既然InheritableThreadLocal在线程池场景下失效,有没有更好的解决方案?

阿里巴巴开源了一个非常实用的库:TransmittableThreadLocal(简称TTL)。它在阿里内部经过大规模生产验证,是解决线程池上下文传递问题的事实标准。

2.1 什么是TransmittableThreadLocal

TransmittableThreadLocal(TTL)是阿里开源的增强版ThreadLocal。它的核心思想是:在提交任务时捕获当前线程的ThreadLocal值,在任务执行时恢复这些值,执行完毕后再清理。

2.2 工作原理

sequenceDiagram
    participant Parent as 父线程
    participant TTL as TTL
    participant Wrapper as TtlRunnable
    participant Pool as 线程池
    participant Worker as 工作线程
    
    Parent->>TTL: set("value")
    Parent->>Wrapper: TtlRunnable.get(task)
    Wrapper->>Wrapper: 捕获当前TTL快照
    Parent->>Pool: submit(wrapper)
    Pool->>Worker: 执行任务
    Worker->>Wrapper: run()
    Wrapper->>Worker: 恢复TTL快照
    Worker->>Worker: 任务执行,可访问TTL
    Wrapper->>Worker: 恢复原始TTL

2.3 使用方法

Maven依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>

方式一:修饰Runnable/Callable

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;

public class TtlDemo1 {
    
    private static final TransmittableThreadLocal<String> CONTEXT = 
        new TransmittableThreadLocal<>();
    
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        
        // 第一次提交
        CONTEXT.set("request-1");
        executor.submit(TtlRunnable.get(() -> {
            System.out.println("任务1: " + CONTEXT.get());  // "request-1"
        })).get();
        
        // 第二次提交(线程被复用)
        CONTEXT.set("request-2");
        executor.submit(TtlRunnable.get(() -> {
            System.out.println("任务2: " + CONTEXT.get());  // "request-2" 
        })).get();
        
        executor.shutdown();
    }
}

方式二:修饰线程池

import com.alibaba.ttl.threadpool.TtlExecutors;

public class TtlDemo2 {
    
    private static final TransmittableThreadLocal<String> CONTEXT = 
        new TransmittableThreadLocal<>();
    
    public static void main(String[] args) throws Exception {
        // 使用TtlExecutors包装线程池
        ExecutorService executor = TtlExecutors.getTtlExecutorService(
            Executors.newFixedThreadPool(1)
        );
        
        CONTEXT.set("request-1");
        executor.submit(() -> {
            System.out.println("任务1: " + CONTEXT.get());  // "request-1"
        }).get();
        
        CONTEXT.set("request-2");
        executor.submit(() -> {
            // 无需TtlRunnable包装,自动传递
            System.out.println("任务2: " + CONTEXT.get());  // "request-2" 
        }).get();
        
        executor.shutdown();
    }
}

2.4 TTL核心源码分析

// TtlRunnable核心逻辑(简化)
public final class TtlRunnable implements Runnable {
    
    private final Runnable runnable;
    // 捕获的TTL快照
    private final Object captured;
    
    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.runnable = runnable;
        // 在构造时捕获当前线程的TTL值
        this.captured = TransmittableThreadLocal.Transmitter.capture();
    }
    
    @Override
    public void run() {
        // 保存工作线程原有的TTL值
        Object backup = TransmittableThreadLocal.Transmitter.replay(captured);
        try {
            // 执行实际任务
            runnable.run();
        } finally {
            // 恢复工作线程原有的TTL值
            TransmittableThreadLocal.Transmitter.restore(backup);
        }
    }
}

2.5 ThreadLocal家族对比

特性ThreadLocalInheritableThreadLocalTransmittableThreadLocal
线程隔离
子线程继承
线程池传递
JDK内置❌(第三方)
性能开销最低中等
使用场景普通场景简单父子线程线程池场景

2.6 异步场景上下文传递方案全景对比

在实际工程中,我们经常遇到需要在异步场景下传递上下文的需求。下面是各种方案的全面对比,帮助你做出正确的技术选型:

方案对比表

方案适用场景优点缺点
显式参数传递简单异步调用清晰、可测试、无魔法侵入性高,需改方法签名
Lambda闭包捕获单次异步调用简单直接只适合简单场景
ThreadLocal + 手动包装线程池无需引入依赖需要手写包装器
TTL线程池成熟稳定、功能完善需引入第三方依赖
Spring TaskDecoratorSpring环境与Spring生态集成好限Spring环境
ScopedValueJDK 21+虚拟线程官方方案、无泄漏风险需JDK 21+

各方案代码示例

// 方案1: 显式参数传递(最推荐)
public void processAsync(String userId, String traceId) {
    executor.submit(() -> {
        doProcess(userId, traceId);  // 直接使用参数
    });
}

// 方案2: Lambda闭包捕获
public void processWithClosure() {
    String userId = UserContext.get();
    String traceId = TraceContext.get();
    executor.submit(() -> {
        // userId和traceId通过闭包捕获
        System.out.println("User: " + userId + ", Trace: " + traceId);
    });
}

// 方案3: 手动包装Runnable
public class ContextAwareRunnable implements Runnable {
    private final Runnable delegate;
    private final String userId;
    private final String traceId;
    
    public ContextAwareRunnable(Runnable delegate) {
        this.delegate = delegate;
        this.userId = UserContext.get();     // 捕获
        this.traceId = TraceContext.get();   // 捕获
    }
    
    @Override
    public void run() {
        try {
            UserContext.set(userId);          // 恢复
            TraceContext.set(traceId);        // 恢复
            delegate.run();
        } finally {
            UserContext.remove();             // 清理
            TraceContext.remove();            // 清理
        }
    }
}

// 方案4: Spring TaskDecorator
@Configuration
public class AsyncConfig {
    
    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(runnable -> {
            // 捕获当前上下文
            String userId = UserContext.get();
            String traceId = TraceContext.get();
            Map<String, String> mdcContext = MDC.getCopyOfContextMap();
            
            return () -> {
                try {
                    // 恢复上下文
                    UserContext.set(userId);
                    TraceContext.set(traceId);
                    if (mdcContext != null) MDC.setContextMap(mdcContext);
                    
                    runnable.run();
                } finally {
                    // 清理
                    UserContext.remove();
                    TraceContext.remove();
                    MDC.clear();
                }
            };
        });
        executor.initialize();
        return executor;
    }
}

常见"不合法姿势"

错误做法问题正确做法
线程池中直接使用ThreadLocal.get()获取到的是工作线程的值,不是提交线程的使用TTL或手动传递
使用InheritableThreadLocal解决线程池问题只在线程创建时复制,复用时不更新使用TTL
CompletableFuture中直接读ThreadLocal可能在ForkJoinPool线程执行,上下文丢失手动传递或TTL
@Async方法中读ThreadLocal异步方法在新线程执行,上下文丢失TaskDecorator或TTL

3. 主流框架中的应用

学习了ThreadLocal的原理和扩展后,让我们看看主流框架是如何使用ThreadLocal的。这些都是经过千锤百炼的实现,值得我们学习和借鉴。

通过分析这些框架的源码,你会发现ThreadLocal在企业级开发中的重要地位,也能学到很多设计技巧。

3.1 Spring框架

Spring框架中大量使用了ThreadLocal,其中最典型的是RequestContextHolderSecurityContextHolder

RequestContextHolder:存储HTTP请求上下文,让我们可以在Service层获取HttpServletRequest:

// Spring源码(简化)
public abstract class RequestContextHolder {
    
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
        new NamedThreadLocal<>("Request attributes");
    
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
        new NamedInheritableThreadLocal<>("Request context");
    
    public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = requestAttributesHolder.get();
        if (attributes == null) {
            attributes = inheritableRequestAttributesHolder.get();
        }
        return attributes;
    }
    
    public static void setRequestAttributes(RequestAttributes attributes, 
                                           boolean inheritable) {
        if (attributes == null) {
            resetRequestAttributes();
        } else {
            if (inheritable) {
                inheritableRequestAttributesHolder.set(attributes);
                requestAttributesHolder.remove();
            } else {
                requestAttributesHolder.set(attributes);
                inheritableRequestAttributesHolder.remove();
            }
        }
    }
}

使用示例

@RestController
public class UserController {
    
    @GetMapping("/user")
    public User getUser() {
        // 获取当前请求
        HttpServletRequest request = ((ServletRequestAttributes) 
            RequestContextHolder.getRequestAttributes()).getRequest();
        
        // 从请求中获取信息
        String token = request.getHeader("Authorization");
        return userService.findByToken(token);
    }
}

3.2 Spring Security

SecurityContextHolder:存储安全上下文

// Spring Security源码(简化)
public class SecurityContextHolder {
    
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    
    private static SecurityContextHolderStrategy strategy;
    
    // ThreadLocal模式实现
    final class ThreadLocalSecurityContextHolderStrategy 
            implements SecurityContextHolderStrategy {
        
        private static final ThreadLocal<SecurityContext> contextHolder = 
            new ThreadLocal<>();
        
        public SecurityContext getContext() {
            SecurityContext ctx = contextHolder.get();
            if (ctx == null) {
                ctx = createEmptyContext();
                contextHolder.set(ctx);
            }
            return ctx;
        }
        
        public void setContext(SecurityContext context) {
            contextHolder.set(context);
        }
        
        public void clearContext() {
            contextHolder.remove();
        }
    }
}

使用示例

@Service
public class SecurityService {
    
    public String getCurrentUsername() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth != null ? auth.getName() : null;
    }
    
    // 异步任务中传递SecurityContext
    @Async
    public void asyncTask() {
        // 默认情况下,异步任务获取不到SecurityContext
        // 需要配置:
        // SecurityContextHolder.setStrategyName(
        //     SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
        
        String username = getCurrentUsername();
    }
}

3.3 Spring事务管理

TransactionSynchronizationManager:存储事务资源

// Spring源码(简化)
public abstract class TransactionSynchronizationManager {
    
    // 当前线程绑定的资源(如DataSource -> Connection)
    private static final ThreadLocal<Map<Object, Object>> resources =
        new NamedThreadLocal<>("Transactional resources");
    
    // 当前线程绑定的事务同步器
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
        new NamedThreadLocal<>("Transaction synchronizations");
    
    // 当前事务名称
    private static final ThreadLocal<String> currentTransactionName =
        new NamedThreadLocal<>("Current transaction name");
    
    // 当前事务是否只读
    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
        new NamedThreadLocal<>("Current transaction read-only status");
    
    // 当前事务隔离级别
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
        new NamedThreadLocal<>("Current transaction isolation level");
    
    // 事务是否激活
    private static final ThreadLocal<Boolean> actualTransactionActive =
        new NamedThreadLocal<>("Actual transaction active");
    
    /**
     * 绑定资源到当前线程
     */
    public static void bindResource(Object key, Object value) {
        Map<Object, Object> map = resources.get();
        if (map == null) {
            map = new HashMap<>();
            resources.set(map);
        }
        map.put(key, value);
    }
    
    /**
     * 获取当前线程绑定的资源
     */
    public static Object getResource(Object key) {
        Map<Object, Object> map = resources.get();
        return map != null ? map.get(key) : null;
    }
}

3.4 MyBatis

SqlSessionManager:存储SqlSession

// MyBatis源码(简化)
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
    
    private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
    
    public void startManagedSession() {
        this.localSqlSession.set(openSession());
    }
    
    public void startManagedSession(boolean autoCommit) {
        this.localSqlSession.set(openSession(autoCommit));
    }
    
    public boolean isManagedSessionStarted() {
        return this.localSqlSession.get() != null;
    }
    
    public void clearManagedSession() {
        this.localSqlSession.remove();
    }
    
    private SqlSession getSqlSession() {
        SqlSession sqlSession = localSqlSession.get();
        if (sqlSession == null) {
            throw new SqlSessionException("...");
        }
        return sqlSession;
    }
}

3.5 Logback MDC

MDC(Mapped Diagnostic Context):存储日志上下文

// Logback MDC实现(简化)
public class LogbackMDCAdapter implements MDCAdapter {
    
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<>();
    
    public void put(String key, String val) {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }
        Map<String, String> oldMap = copyOnThreadLocal.get();
        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
        
        if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
            Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
            newMap.put(key, val);
        } else {
            oldMap.put(key, val);
        }
    }
    
    public String get(String key) {
        Map<String, String> map = copyOnThreadLocal.get();
        return map != null ? map.get(key) : null;
    }
    
    public void remove(String key) {
        Map<String, String> oldMap = copyOnThreadLocal.get();
        if (oldMap != null) {
            oldMap.remove(key);
        }
    }
    
    public void clear() {
        copyOnThreadLocal.remove();
    }
}

使用示例

// 在过滤器中设置
public class TraceFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        try {
            String traceId = UUID.randomUUID().toString();
            MDC.put("traceId", traceId);
            MDC.put("userId", getUserId(request));
            
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

// logback.xml配置
// <pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{userId}] %-5level %logger{36} - %msg%n</pattern>

3.6 Dubbo RpcContext

在微服务架构中,Dubbo使用RpcContext在服务调用链中传递上下文信息。RpcContext内部也是基于ThreadLocal实现的:

// Dubbo RpcContext源码(简化)
public class RpcContext {
    
    private static final InternalThreadLocal<RpcContext> LOCAL = 
        new InternalThreadLocal<RpcContext>() {
            @Override
            protected RpcContext initialValue() {
                return new RpcContext();
            }
        };
    
    // 附件信息,用于隐式传参
    private final Map<String, Object> attachments = new HashMap<>();
    
    public static RpcContext getContext() {
        return LOCAL.get();
    }
    
    public RpcContext setAttachment(String key, Object value) {
        attachments.put(key, value);
        return this;
    }
    
    public Object getAttachment(String key) {
        return attachments.get(key);
    }
    
    // 每次RPC调用后会自动清理
    public static void removeContext() {
        LOCAL.remove();
    }
}

Dubbo中传递TraceId示例

// 消费端Filter:传递TraceId
public class TraceIdConsumerFilter implements Filter {
    
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) {
        // 从当前上下文获取TraceId,设置到RpcContext
        String traceId = TraceContext.getTraceId();
        RpcContext.getContext().setAttachment("traceId", traceId);
        
        return invoker.invoke(invocation);
    }
}

// 提供端Filter:接收TraceId
public class TraceIdProviderFilter implements Filter {
    
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) {
        // 从RpcContext获取TraceId,设置到当前上下文
        String traceId = (String) RpcContext.getContext().getAttachment("traceId");
        if (traceId != null) {
            TraceContext.setTraceId(traceId);
            MDC.put("traceId", traceId);
        }
        
        try {
            return invoker.invoke(invocation);
        } finally {
            TraceContext.clear();
            MDC.remove("traceId");
        }
    }
}

3.7 安全警示:ThreadLocal中的敏感数据

安全警告:ThreadLocal中存储敏感数据需要特别注意!

在用户上下文传递场景中,如果ThreadLocal存储了敏感信息(如用户密码、Token、权限列表),线程池复用导致的数据污染可能引发严重的安全漏洞

风险场景

风险描述后果
权限绕过用户A的权限信息残留,被用户B的请求读取越权访问
数据泄露用户A的敏感数据被用户B看到隐私泄露
身份冒充用户B以用户A的身份执行操作审计失效

安全最佳实践

// 危险:存储完整的敏感对象
public class UnsafeContext {
    private static final ThreadLocal<User> USER = new ThreadLocal<>();
    
    @Data
    public static class User {
        private String userId;
        private String password;      // 危险!
        private String token;         // 危险!
        private List<String> roles;   // 可能泄露
    }
}

// 安全:最小化存储 + 脱敏
public class SafeContext {
    private static final ThreadLocal<SafeUserInfo> USER = new ThreadLocal<>();
    
    @Data
    public static class SafeUserInfo {
        private String userId;        // 仅存储ID
        private String username;      // 脱敏后的用户名
        // 不存储密码、token等敏感信息
        // 权限信息每次从数据库/缓存实时获取
    }
}

//  关键操作:双重校验
public void deleteUser(Long targetUserId) {
    SafeUserInfo operator = SafeContext.USER.get();
    
    // 1. ThreadLocal提供便利(但不作为唯一依据)
    if (operator == null) {
        throw new SecurityException("未登录");
    }
    
    // 2. 关键操作从权限服务重新校验(防止ThreadLocal污染)
    boolean hasPermission = permissionService.checkPermission(
        operator.getUserId(), "user:delete");
    
    if (!hasPermission) {
        throw new SecurityException("无权限");
    }
    
    userRepository.delete(targetUserId);
}

安全检查清单

检查项说明
不存储密码、Token等凭证即使泄露也无法直接利用
最小化存储原则只存储必要的非敏感字段
关键操作二次校验不完全信任ThreadLocal的值
敏感操作审计日志记录实际执行者信息
定期安全审查检查ThreadLocal使用是否合规

参考OWASP Session Management Cheat Sheet


4. 自定义ThreadLocal扩展

4.1 带过期时间的ThreadLocal

/**
 * 支持过期时间的ThreadLocal
 */
public class ExpirableThreadLocal<T> extends ThreadLocal<T> {
    
    private final ThreadLocal<Long> expirationTime = new ThreadLocal<>();
    private final long ttlMillis;
    
    public ExpirableThreadLocal(long ttlMillis) {
        this.ttlMillis = ttlMillis;
    }
    
    @Override
    public void set(T value) {
        super.set(value);
        expirationTime.set(System.currentTimeMillis() + ttlMillis);
    }
    
    @Override
    public T get() {
        Long expTime = expirationTime.get();
        if (expTime != null && System.currentTimeMillis() > expTime) {
            remove();
            return null;
        }
        return super.get();
    }
    
    @Override
    public void remove() {
        super.remove();
        expirationTime.remove();
    }
}

// 使用
ExpirableThreadLocal<String> cache = new ExpirableThreadLocal<>(5000);  // 5秒过期
cache.set("value");
Thread.sleep(6000);
cache.get();  // null,已过期

4.2 可观测的ThreadLocal

/**
 * 支持监控的ThreadLocal
 */
public class ObservableThreadLocal<T> extends ThreadLocal<T> {
    
    private static final AtomicLong setCount = new AtomicLong();
    private static final AtomicLong getCount = new AtomicLong();
    private static final AtomicLong removeCount = new AtomicLong();
    
    private final String name;
    
    public ObservableThreadLocal(String name) {
        this.name = name;
    }
    
    @Override
    public void set(T value) {
        setCount.incrementAndGet();
        log.debug("[{}] set called, value: {}", name, value);
        super.set(value);
    }
    
    @Override
    public T get() {
        getCount.incrementAndGet();
        T value = super.get();
        log.debug("[{}] get called, value: {}", name, value);
        return value;
    }
    
    @Override
    public void remove() {
        removeCount.incrementAndGet();
        log.debug("[{}] remove called", name);
        super.remove();
    }
    
    public static Map<String, Long> getMetrics() {
        Map<String, Long> metrics = new HashMap<>();
        metrics.put("setCount", setCount.get());
        metrics.put("getCount", getCount.get());
        metrics.put("removeCount", removeCount.get());
        return metrics;
    }
}

4.3 类型安全的上下文

/**
 * 类型安全的上下文Key
 */
public final class ContextKey<T> {
    
    private final String name;
    private final Class<T> type;
    
    private ContextKey(String name, Class<T> type) {
        this.name = name;
        this.type = type;
    }
    
    public static <T> ContextKey<T> of(String name, Class<T> type) {
        return new ContextKey<>(name, type);
    }
    
    public String getName() { return name; }
    public Class<T> getType() { return type; }
}

/**
 * 类型安全的ThreadLocal上下文
 */
public class TypedThreadContext {
    
    private static final ThreadLocal<Map<ContextKey<?>, Object>> CONTEXT = 
        ThreadLocal.withInitial(HashMap::new);
    
    public static <T> void set(ContextKey<T> key, T value) {
        CONTEXT.get().put(key, value);
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T get(ContextKey<T> key) {
        return (T) CONTEXT.get().get(key);
    }
    
    public static <T> void remove(ContextKey<T> key) {
        CONTEXT.get().remove(key);
    }
    
    public static void clear() {
        CONTEXT.remove();
    }
}

// 使用
ContextKey<String> USER_ID = ContextKey.of("userId", String.class);
ContextKey<Integer> TENANT_ID = ContextKey.of("tenantId", Integer.class);

TypedThreadContext.set(USER_ID, "user-001");
TypedThreadContext.set(TENANT_ID, 123);

String userId = TypedThreadContext.get(USER_ID);      // 类型安全
Integer tenantId = TypedThreadContext.get(TENANT_ID); // 类型安全

5. ThreadLocal vs 其他方案对比

5.1 ThreadLocal vs 参数传递

维度ThreadLocal参数传递
侵入性低,无需修改方法签名高,需要修改所有方法
可读性低,隐式依赖高,依赖显式
调试难度高,值来源不明显低,可追踪
性能O(1)哈希访问方法调用开销
适用场景横切关注点(日志、安全)业务数据流转

5.2 ThreadLocal vs Scope Bean(Spring)

// ThreadLocal方式
@Component
public class RequestContext {
    private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
    
    public void setRequestId(String id) { REQUEST_ID.set(id); }
    public String getRequestId() { return REQUEST_ID.get(); }
    public void clear() { REQUEST_ID.remove(); }
}

// Spring Request Scope方式
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedContext {
    private String requestId;
    
    public void setRequestId(String id) { this.requestId = id; }
    public String getRequestId() { return this.requestId; }
    // 不需要手动清理,Spring自动管理
}
维度ThreadLocalScope Bean
生命周期管理手动自动
框架依赖需要Spring
异步传递需要处理需要处理
适用范围任何场景Web请求场景

5.3 选型建议

17_ThreadLocal选型建议.drawio.png


6. JDK版本演进

6.1 版本特性对比

JDK版本新增特性
JDK 1.2ThreadLocal首次引入
JDK 1.2InheritableThreadLocal引入
JDK 1.4ThreadLocalMap使用弱引用
JDK 1.5增加remove()方法
JDK 1.8增加withInitial()工厂方法
JDK 1.8SuppliedThreadLocal内部类

6.2 JDK 8+ 推荐写法

// JDK 8之前
private static final ThreadLocal<SimpleDateFormat> OLD_STYLE = 
    new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

// JDK 8+ 推荐
private static final ThreadLocal<SimpleDateFormat> NEW_STYLE = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// 方法引用
private static final ThreadLocal<StringBuilder> BUILDER = 
    ThreadLocal.withInitial(StringBuilder::new);

6.3 Virtual Threads(JDK 21+)与ThreadLocal

JDK 21正式引入了虚拟线程(Virtual Threads),这对ThreadLocal的使用带来了重大影响。

虚拟线程的特点

  • 数量可能非常大(百万级)
  • 生命周期通常很短
  • 由JVM调度,可能在不同的载体线程(carrier thread)上运行

ThreadLocal在虚拟线程中的问题

问题影响严重程度
内存占用百万虚拟线程 × N个ThreadLocal = 巨大内存开销严重
频繁创建/销毁虚拟线程生命周期短,ThreadLocal初始化开销累积中等
载体线程切换虚拟线程可能在不同载体线程上恢复执行低(JVM处理)
// 虚拟线程中的问题示例
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> {
            // 每个虚拟线程都有独立的ThreadLocalMap
            // 100万个虚拟线程 = 100万份ThreadLocal副本!
            USER_CONTEXT.set(currentUser);
            try {
                processRequest();
            } finally {
                USER_CONTEXT.remove();
            }
        });
    }
}

工程建议:在虚拟线程场景下,优先考虑显式参数传递ScopedValue

6.4 ScopedValue(JDK 21+ 预览特性)

ScopedValue是JDK 21引入的预览特性(JDK 23正式发布),专门设计用于替代ThreadLocal在特定场景下的使用。

ScopedValue vs ThreadLocal 对比

维度ThreadLocalScopedValue
生命周期绑定线程,需手动清理绑定代码块/作用域,自动清理
可变性可变(可set多次)不可变(一次绑定)
继承性需要InheritableThreadLocal天然支持结构化并发
内存泄漏风险高(忘记remove)(作用域结束自动释放)
虚拟线程支持内存开销大优化设计,开销小
可见性线程内全局可见仅在绑定的作用域内可见

ScopedValue使用示例(JDK 21+ 预览特性):

import jdk.incubator.concurrent.ScopedValue;  // JDK 21预览
// import java.lang.ScopedValue;  // JDK 23+正式版

public class ScopedValueDemo {
    
    // 声明ScopedValue(类似ThreadLocal的声明)
    private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
    
    public void handleRequest(String userId) {
        // 绑定值并执行代码块
        ScopedValue.where(USER_ID, userId).run(() -> {
            // 在此作用域内,USER_ID.get() 返回 userId
            processRequest();
            // 作用域结束,自动清理,无需手动remove!
        });
    }
    
    private void processRequest() {
        // 可以在调用链任意位置获取值
        String userId = USER_ID.get();
        System.out.println("处理用户: " + userId);
        
        // 支持嵌套作用域
        ScopedValue.where(USER_ID, "nested-user").run(() -> {
            System.out.println("嵌套作用域: " + USER_ID.get());  // "nested-user"
        });
        
        // 嵌套结束后,恢复原值
        System.out.println("恢复: " + USER_ID.get());  // userId
    }
    
    // 支持带返回值的调用
    public String processWithResult(String userId) {
        return ScopedValue.where(USER_ID, userId).call(() -> {
            return "处理结果: " + USER_ID.get();
        });
    }
}

何时使用ScopedValue替代ThreadLocal

场景推荐方案原因
只读上下文传递(如userId, traceId)ScopedValue不可变,无泄漏风险
可变状态绑定线程(如Connection)ThreadLocal需要可变性
虚拟线程场景ScopedValue内存优化
结构化并发(StructuredTaskScope)ScopedValue天然支持
传统线程池 + 老项目ThreadLocal + TTL兼容性

6.5 从ThreadLocal迁移到ScopedValue

如果你的项目使用JDK 21+,并且满足以下条件,可以考虑迁移:

适合迁移的场景

  1. ThreadLocal存储的是只读数据(如用户ID、请求ID)
  2. 值在请求/任务开始时设置,结束后不再需要
  3. 使用虚拟线程或计划使用
  4. 希望消除内存泄漏风险

迁移步骤

// Step 1: 原ThreadLocal代码
public class OldStyle {
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
    
    public void handle(String userId) {
        try {
            USER_ID.set(userId);
            process();
        } finally {
            USER_ID.remove();
        }
    }
}

// Step 2: 迁移到ScopedValue
public class NewStyle {
    private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
    
    public void handle(String userId) {
        ScopedValue.where(USER_ID, userId).run(this::process);
        // 无需try-finally,自动清理!
    }
}

// Step 3: 兼容模式(同时支持两种方式)
public class CompatibleStyle {
    private static final ScopedValue<String> USER_ID_SCOPED = ScopedValue.newInstance();
    private static final ThreadLocal<String> USER_ID_TL = new ThreadLocal<>();
    
    public static String getUserId() {
        // 优先从ScopedValue获取,回退到ThreadLocal
        if (USER_ID_SCOPED.isBound()) {
            return USER_ID_SCOPED.get();
        }
        return USER_ID_TL.get();
    }
}

注意:ScopedValue在JDK 21/22是预览特性,需要添加--enable-preview参数。JDK 23+正式发布。

6.6 日期格式化:新旧方案对比

文章前面使用ThreadLocal<SimpleDateFormat>作为经典示例,这主要是为老项目准备的。新项目应直接使用DateTimeFormatter

//  老项目方案(仍然有效,但不推荐新项目使用)
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public String formatOld(Date date) {
    return DATE_FORMAT.get().format(date);
}

// JDK 8+新项目推荐:DateTimeFormatter是线程安全的,无需ThreadLocal
private static final DateTimeFormatter DATE_FORMAT = 
    DateTimeFormatter.ofPattern("yyyy-MM-dd");

public String formatNew(LocalDate date) {
    return date.format(DATE_FORMAT);  // 直接使用,线程安全
}

// 如果需要处理java.util.Date(兼容老代码)
public String formatNewFromDate(Date date) {
    return date.toInstant()
        .atZone(ZoneId.systemDefault())
        .toLocalDate()
        .format(DATE_FORMAT);
}

ThreadLocal家族速查表(2025更新版)

类型适用场景JDK版本注意事项
ThreadLocal普通线程隔离1.2+必须手动remove
InheritableThreadLocal简单父子线程1.2+线程池失效
TransmittableThreadLocal线程池场景第三方需要引入阿里TTL依赖
ScopedValue只读上下文、虚拟线程21+预览/23+正式不可变,自动清理

总结

又是没有大厂约面日子😣😣😣,小编还在找实习的路上,这篇文章是我的笔记汇总整理。

参考资源