ThreadLocal深入剖析

前言

在并发编程中,当多个线程同时操作一个共享变量,就会出现线程安全问题。常见的解决方案是加锁,但锁会带来性能开销,尤其在高并发场景下。今天要讲的 ThreadLocal,提供了另一种思路:让每个线程拥有自己独立的变量副本,从根本上避免竞争。

🏠个人主页:你的主页


文章目录


一、线程安全问题的两种场景

在讨论 ThreadLocal 之前,我们先搞清楚:什么场景该用锁,什么场景该用 ThreadLocal

1.1 场景一:多线程竞争修改同一个值

场景描述:电商系统的商品秒杀,100 件商品,多个用户同时抢购。

public class SeckillService {
    private int stock = 100;  // 库存,共享变量
    
    public void seckill() {
        if (stock > 0) {
            stock--;  // 扣减库存
            System.out.println(Thread.currentThread().getName() + " 抢购成功,剩余库存:" + stock);
        } else {
            System.out.println(Thread.currentThread().getName() + " 抢购失败,库存不足");
        }
    }
}

问题:两个线程同时判断 stock > 0,都通过了,然后都执行 stock--,导致超卖

线程A:读取 stock = 1,判断 > 0,准备扣减
线程B:读取 stock = 1,判断 > 0,准备扣减
线程A:stock-- → stock = 0
线程B:stock-- → stock = -1  ❌ 超卖了!

解决方案:加锁

public synchronized void seckill() {
    if (stock > 0) {
        stock--;
        System.out.println(Thread.currentThread().getName() + " 抢购成功");
    }
}

能用 ThreadLocal 吗?不能!

因为 ThreadLocal 会给每个线程创建独立副本,每个线程都有自己的 stock = 100,那每个人都能抢 100 件,这显然不对。

结论:当多个线程需要竞争修改同一个值时,必须用锁,不能用 ThreadLocal。

1.2 场景二:每个线程需要独立的上下文数据

场景描述:Web 系统中,每个请求需要携带当前登录用户的信息,在整个请求链路中随时可以获取。

用户A发起请求 → 拦截器解析Token获取用户A信息 → Controller → Service → DAO
用户B发起请求 → 拦截器解析Token获取用户B信息 → Controller → Service → DAO

如果用普通的共享变量存储用户信息:

public class UserContext {
    public static User currentUser;  // 共享变量
}

问题

线程A:设置 currentUser = 用户A
线程B:设置 currentUser = 用户B  ← 覆盖了!
线程A:获取 currentUser → 拿到的是用户B  ❌ 数据错乱!

解决方案:ThreadLocal

public class UserContext {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
    
    public static void setUser(User user) {
        currentUser.set(user);
    }
    
    public static User getUser() {
        return currentUser.get();
    }
}

每个线程都有自己独立的 User 副本,互不干扰。

结论:当每个线程需要独立的上下文数据,且这个数据在整个线程生命周期内需要被多处访问时,用 ThreadLocal。

1.3 两种场景对比

场景 特点 解决方案
秒杀库存 多线程竞争修改同一个值 加锁(synchronized/Lock)
用户上下文 每个线程需要独立的值 ThreadLocal

一句话总结

  • 锁:让多个线程排队操作同一个变量
  • ThreadLocal:让每个线程各自操作自己的变量

二、ThreadLocal的正确使用场景

2.1 场景一:请求上下文传递

这是 ThreadLocal 最经典的使用场景。

需求:在 Web 应用中,用户登录后,整个请求链路都需要获取当前用户信息。

传统做法:把用户信息作为参数层层传递

// Controller
public void createOrder(User user, OrderDTO orderDTO) {
    orderService.createOrder(user, orderDTO);
}

// Service
public void createOrder(User user, OrderDTO orderDTO) {
    // 业务逻辑
    orderDao.insert(user, order);
    logService.log(user, "创建订单");
}

问题:参数传递太繁琐,代码侵入性强。

ThreadLocal 做法

// 定义用户上下文
public class UserContextHolder {
    private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userHolder.set(user);
    }
    
    public static User getUser() {
        return userHolder.get();
    }
    
    public static void clear() {
        userHolder.remove();
    }
}

// 拦截器中设置
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        User user = tokenService.parseToken(token);
        UserContextHolder.setUser(user);  // 存入 ThreadLocal
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        UserContextHolder.clear();  // 请求结束,清理 ThreadLocal
    }
}

// 业务代码中随时获取
public void createOrder(OrderDTO orderDTO) {
    User user = UserContextHolder.getUser();  // 直接获取,无需传参
    // 业务逻辑...
}

2.2 场景二:数据库连接管理

需求:保证同一个线程内的多次数据库操作使用同一个连接,实现事务。

public class ConnectionManager {
    private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
    
    public static Connection getConnection() throws SQLException {
        Connection conn = connectionHolder.get();
        if (conn == null) {
            conn = dataSource.getConnection();
            connectionHolder.set(conn);
        }
        return conn;
    }
    
    public static void closeConnection() {
        Connection conn = connectionHolder.get();
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            connectionHolder.remove();
        }
    }
}

Spring 的 @Transactional 底层就是用 ThreadLocal 来保证同一事务内使用同一个数据库连接。

2.3 场景三:日期格式化工具

问题SimpleDateFormat 不是线程安全的。

// 错误示范:多线程共享 SimpleDateFormat
public class DateUtil {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    
    public static String format(Date date) {
        return sdf.format(date);  // 多线程调用会出问题!
    }
}

ThreadLocal 解决

public class DateUtil {
    private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    
    public static String format(Date date) {
        return dateFormatHolder.get().format(date);  // 每个线程用自己的实例
    }
}

2.4 场景四:链路追踪 TraceId

需求:分布式系统中,一个请求会经过多个服务,需要用 TraceId 串联整个调用链路。

public class TraceContext {
    private static final ThreadLocal<String> traceIdHolder = new ThreadLocal<>();
    
    public static void setTraceId(String traceId) {
        traceIdHolder.set(traceId);
    }
    
    public static String getTraceId() {
        return traceIdHolder.get();
    }
    
    public static void clear() {
        traceIdHolder.remove();
    }
}

// 在日志中自动带上 TraceId
public void doSomething() {
    log.info("[{}] 开始处理业务", TraceContext.getTraceId());
    // 业务逻辑...
}

三、ThreadLocal核心API

3.1 基本方法

ThreadLocal<String> threadLocal = new ThreadLocal<>();

// 设置值
threadLocal.set("hello");

// 获取值
String value = threadLocal.get();  // "hello"

// 移除值(重要!防止内存泄漏)
threadLocal.remove();

3.2 初始值设置

方式一:重写 initialValue 方法

ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

方式二:使用 withInitial(推荐,Java 8+)

ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
ThreadLocal<List<String>> listHolder = ThreadLocal.withInitial(ArrayList::new);

3.3 完整示例

public class ThreadLocalDemo {
    
    private static final ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
    
    public static void main(String[] args) {
        // 线程1
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                int value = counter.get();
                counter.set(value + 1);
                System.out.println(Thread.currentThread().getName() + ": " + counter.get());
            }
        }, "Thread-A").start();
        
        // 线程2
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                int value = counter.get();
                counter.set(value + 1);
                System.out.println(Thread.currentThread().getName() + ": " + counter.get());
            }
        }, "Thread-B").start();
    }
}

输出(顺序可能不同,但每个线程独立计数):

Thread-A: 1
Thread-A: 2
Thread-A: 3
Thread-B: 1
Thread-B: 2
Thread-B: 3

两个线程各自从 0 开始计数,互不影响。


四、底层数据结构演进

4.1 Java 8 之前的设计

ThreadLocal 对象
    └── ThreadLocalMap
            ├── Entry(Thread-A, value-A)
            ├── Entry(Thread-B, value-B)
            └── Entry(Thread-C, value-C)

特点

  • ThreadLocal 维护一个 Map
  • Key 是 Thread 对象
  • Value 是变量副本

问题

  • 需要对 Map 加锁,因为多个线程会同时操作这个 Map
  • 线程销毁后,对应的 Entry 不会自动清理

4.2 Java 8 之后的设计

Thread-A 对象
    └── ThreadLocalMap
            ├── Entry(ThreadLocal-1, value-1)
            ├── Entry(ThreadLocal-2, value-2)
            └── Entry(ThreadLocal-3, value-3)

Thread-B 对象
    └── ThreadLocalMap
            ├── Entry(ThreadLocal-1, value-1)
            └── Entry(ThreadLocal-2, value-2)

特点

  • 每个 Thread 对象内部维护一个 ThreadLocalMap
  • Key 是 ThreadLocal 对象
  • Value 是变量副本

优势

  • 无需加锁,因为每个线程只操作自己的 Map
  • 线程销毁时,ThreadLocalMap 随之销毁,Entry 自动回收

4.3 源码分析

Thread 类中的字段

public class Thread implements Runnable {
    // 每个线程都有自己的 ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal.set() 方法

public void set(T value) {
    Thread t = Thread.currentThread();  // 获取当前线程
    ThreadLocalMap map = getMap(t);     // 获取当前线程的 ThreadLocalMap
    if (map != null) {
        map.set(this, value);           // Key 是当前 ThreadLocal 对象
    } else {
        createMap(t, value);            // 首次使用,创建 Map
    }
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;              // 返回线程的 threadLocals 字段
}

ThreadLocal.get() 方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            return (T) e.value;
        }
    }
    return setInitialValue();  // 没有值则返回初始值
}

4.4 ThreadLocalMap 的结构

ThreadLocalMap 是 ThreadLocal 的静态内部类,使用数组 + 线性探测法解决哈希冲突。

static class ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        
        Entry(ThreadLocal<?> k, Object v) {
            super(k);  // Key 是弱引用
            value = v;
        }
    }
    
    private Entry[] table;  // Entry 数组
}

关键点:Entry 的 Key(ThreadLocal 对象)是弱引用


五、ThreadLocal内存泄漏问题

5.1 为什么会内存泄漏

先看 Entry 的结构:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;  // Value 是强引用
}
  • Key(ThreadLocal):弱引用,GC 时会被回收
  • Value:强引用,不会被自动回收

泄漏场景

1. ThreadLocal 对象被设为 null
2. GC 回收 ThreadLocal 对象(因为是弱引用)
3. Entry 的 Key 变成 null,但 Value 还在
4. 如果线程一直存活(比如线程池),这个 Value 永远无法被回收
Thread
  └── ThreadLocalMap
        └── Entry
              ├── Key: null(已被 GC 回收)
              └── Value: 还在!无法回收!← 内存泄漏

5.2 为什么 Key 要设计成弱引用

如果 Key 是强引用:

ThreadLocal tl = new ThreadLocal();
tl.set("value");
tl = null;  // 想释放 ThreadLocal

// 但是 Entry 还持有 ThreadLocal 的强引用
// ThreadLocal 对象无法被回收 ← 更严重的内存泄漏!

设计成弱引用后,至少 ThreadLocal 对象可以被回收,只是 Value 还在。

5.3 ThreadLocal 的自我清理机制

ThreadLocal 在 get()set()remove() 时会顺便清理 Key 为 null 的 Entry:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    // 清理 Key 为 null 的 Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // ...
}

但这个清理是被动的,如果一直不调用这些方法,泄漏的 Value 就一直存在。

5.4 正确的使用姿势

原则:用完必须调用 remove()

public class UserContextHolder {
    private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userHolder.set(user);
    }
    
    public static User getUser() {
        return userHolder.get();
    }
    
    // 关键:提供 clear 方法
    public static void clear() {
        userHolder.remove();
    }
}

// 使用时
try {
    UserContextHolder.setUser(user);
    // 业务逻辑...
} finally {
    UserContextHolder.clear();  // 必须清理!
}

在 Web 应用中

public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(...) {
        UserContextHolder.setUser(user);
        return true;
    }
    
    @Override
    public void afterCompletion(...) {
        UserContextHolder.clear();  // 请求结束时清理
    }
}

六、实战最佳实践

6.1 完整的上下文工具类

public class RequestContext {
    
    private static final ThreadLocal<Map<String, Object>> CONTEXT = 
        ThreadLocal.withInitial(HashMap::new);
    
    // 设置属性
    public static void set(String key, Object value) {
        CONTEXT.get().put(key, value);
    }
    
    // 获取属性
    @SuppressWarnings("unchecked")
    public static <T> T get(String key) {
        return (T) CONTEXT.get().get(key);
    }
    
    // 获取属性,带默认值
    @SuppressWarnings("unchecked")
    public static <T> T get(String key, T defaultValue) {
        T value = (T) CONTEXT.get().get(key);
        return value != null ? value : defaultValue;
    }
    
    // 移除属性
    public static void remove(String key) {
        CONTEXT.get().remove(key);
    }
    
    // 清空所有(重要!)
    public static void clear() {
        CONTEXT.remove();
    }
    
    // 常用快捷方法
    public static void setUserId(Long userId) {
        set("userId", userId);
    }
    
    public static Long getUserId() {
        return get("userId");
    }
    
    public static void setTraceId(String traceId) {
        set("traceId", traceId);
    }
    
    public static String getTraceId() {
        return get("traceId");
    }
}

6.2 配合 Spring 拦截器使用

@Component
public class ContextInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 生成 TraceId
        String traceId = UUID.randomUUID().toString().replace("-", "");
        RequestContext.setTraceId(traceId);
        
        // 解析用户信息
        String token = request.getHeader("Authorization");
        if (StringUtils.hasText(token)) {
            Long userId = tokenService.parseUserId(token);
            RequestContext.setUserId(userId);
        }
        
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) {
        // 请求结束,必须清理
        RequestContext.clear();
    }
}

6.3 线程池场景的注意事项

线程池中的线程是复用的,如果不清理 ThreadLocal,下一个任务会拿到上一个任务的数据。

ExecutorService executor = Executors.newFixedThreadPool(2);

executor.submit(() -> {
    try {
        RequestContext.setUserId(100L);
        // 业务逻辑...
    } finally {
        RequestContext.clear();  // 必须清理!
    }
});

更优雅的方式:封装任务包装器

public class ContextAwareRunnable implements Runnable {
    
    private final Runnable task;
    private final Map<String, Object> context;
    
    public ContextAwareRunnable(Runnable task) {
        this.task = task;
        // 捕获提交任务时的上下文
        this.context = new HashMap<>(RequestContext.getAll());
    }
    
    @Override
    public void run() {
        try {
            // 恢复上下文
            RequestContext.setAll(context);
            task.run();
        } finally {
            // 清理上下文
            RequestContext.clear();
        }
    }
}

// 使用
executor.submit(new ContextAwareRunnable(() -> {
    // 可以获取到父线程的上下文
    Long userId = RequestContext.getUserId();
}));

七、InheritableThreadLocal

7.1 问题:子线程无法获取父线程的 ThreadLocal

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("父线程的值");

new Thread(() -> {
    System.out.println(threadLocal.get());  // null!
}).start();

子线程无法获取父线程设置的值。

7.2 解决方案:InheritableThreadLocal

InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("父线程的值");

new Thread(() -> {
    System.out.println(inheritableThreadLocal.get());  // "父线程的值"
}).start();

原理:创建子线程时,会把父线程的 InheritableThreadLocal 值复制一份给子线程。

7.3 局限性

InheritableThreadLocal 只在创建线程时复制,对于线程池场景不适用:

ExecutorService executor = Executors.newFixedThreadPool(1);

inheritableThreadLocal.set("任务1的值");
executor.submit(() -> {
    System.out.println(inheritableThreadLocal.get());  // "任务1的值" ✅
});

inheritableThreadLocal.set("任务2的值");
executor.submit(() -> {
    System.out.println(inheritableThreadLocal.get());  // 还是"任务1的值" ❌
});

因为线程池复用线程,第二个任务用的是已经创建好的线程,不会重新复制。

解决方案:使用阿里开源的 TransmittableThreadLocal(TTL)


八、总结

8.1 核心要点

要点 说明
适用场景 每个线程需要独立的上下文数据
不适用场景 多线程竞争修改同一个值
底层结构 每个 Thread 持有 ThreadLocalMap,Key 是 ThreadLocal
内存泄漏 Key 是弱引用会被回收,Value 是强引用不会自动回收
最佳实践 用完必须调用 remove()

8.2 使用场景总结

场景 示例
请求上下文 用户信息、租户信息、TraceId
数据库连接 同一事务内复用连接
日期格式化 SimpleDateFormat 线程安全问题
分布式追踪 链路追踪 TraceId 传递

8.3 ThreadLocal vs 锁

对比项 ThreadLocal 锁(synchronized/Lock)
解决思路 空间换时间,每个线程一份副本 时间换安全,排队访问
适用场景 线程隔离,各自操作各自的数据 线程同步,共同操作同一份数据
性能 无竞争,性能高 有竞争,性能相对低
数据一致性 不保证(各自独立) 保证(同一份数据)

记住:ThreadLocal 不是用来解决共享变量的线程安全问题的,而是用来实现线程隔离的。


热门专栏推荐

等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐