ThreadLocal

87 阅读3分钟

基础概念

什么是ThreadLocal

ThreadLocal 是 Java 中一个特殊的类,它为每个使用该变量的线程提供独立的变量副本,实现了线程间的数据隔离

核心特点:

  1. 线程隔离:每个线程访问的是自己的变量副本,互不干扰
  2. 避免同步:由于数据不共享,无需同步操作
  3. 内存泄漏风险:使用不当可能导致内存泄漏

工作原理

ThreadLocal 内部使用 ThreadLocalMap 存储数据:

  • 每个 Thread 对象内部都有一个 ThreadLocalMap
  • ThreadLocal 作为 Map 的 key
  • set()/get() 操作实际是在当前线程的 ThreadLocalMap 上工作

源码如下:

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

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;  // 直接返回线程的成员变量
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

public T get() {
    return get(Thread.currentThread());
}


private T get(Thread t) {
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }
    // 返回初始值,默认是null,也可以通过重写 initialValue()方法为ThreadLocal 变量提供初始值
    return setInitialValue(t); 
}

ThreadLocal 高级特性

InheritableThreadLocal

InheritableThreadLocal 是 ThreadLocal 的子类,它允许子线程继承父线程的线程局部变量值

特点:

  • 当创建新线程时,新线程会继承父线程的所有可继承线程局部变量
  • 适用于需要在父子线程间传递上下文信息的场景
public class UserContext {
    private static final InheritableThreadLocal<User> currentUser = 
        new InheritableThreadLocal<>();

    public static void setCurrentUser(User user) {
        currentUser.set(user);
    }

    public static User getCurrentUser() {
        return currentUser.get();
    }

    public static void clear() {
        currentUser.remove();
    }
}

// 业务使用示例
public class ReportGenerationService {
    public void generateReportAsync(User user, ReportRequest request) {
        UserContext.setCurrentUser(user);
        
        try {
            // 主线程准备数据
            prepareData(request);
            
            // 异步生成报告(可能耗时)
            new Thread(() -> {
                try {
                    // 子线程可以获取用户信息
                    User currentUser = UserContext.getCurrentUser();
                    if (!currentUser.hasPermission("report.generate")) {
                        throw new SecurityException("无权限生成报告");
                    }
                    
                    // 生成报告逻辑...
                    generatePDFReport(request, currentUser);
                } finally {
                    UserContext.clear();
                }
            }).start();
        } finally {
            UserContext.clear();
        }
    }
}

ThreadLocal 与内存泄漏

  • ThreadLocal 的键是弱引用,但值是强引用
  • 如果线程长时间运行且不调用 remove(),可能导致值无法被回收

强引用和弱引用:

引用类型决定了对象如何被垃圾回收器(GC)处理,不同引用类型对内存管理有重要影响

  • 强引用:

    • 默认的引用类型:普通对象赋值(如 Object obj = new Object())就是强引用
    • GC行为:只要强引用存在,对象就不会被回收,即使内存不足时抛出 OutOfMemoryError
    • 生命周期:对象的生命周期由强引用决定,手动置为 null 或超出作用域后才会成为垃圾回收候选
    Object strongRef = new Object(); // 强引用
    strongRef = null; // 手动断开强引用,此时对象可被GC回收
    
  • 弱引用:

    • 通过 WeakReference 类实现:需要显式使用 java.lang.ref.WeakReference
    • GC行为:当只有弱引用指向对象时,无论内存是否充足,GC都会回收该对象
    • 生命周期:对象存活到下一次GC运行
    Object obj = new Object();
    WeakReference<​Object> weakRef = new WeakReference<>(obj); // 创建弱引用
    obj = null; // 断开强引用
    System.gc(); // 触发GC(仅示例,不保证立即执行)
     
    // 弱引用的对象可能已被回收
    System.out.println(weakRef.get()); // 可能输出 null
    

此外还有软引用和虚引用,这里不做多介绍

ThreadLocal与线程池

值污染

值污染发生在同一个线程被不同任务复用时,前一个任务设置的ThreadLocal值未被清理,被后一个任务意外读取

final ThreadLocal<Integer> taskIdHolder = new ThreadLocal<>();
ExecutorService pool = Executors.newSingleThreadExecutor(); // 单线程池

// 任务1:设置值但忘记清理
pool.execute(() -> {
    taskIdHolder.set(1);
    System.out.println("任务1设置: " + taskIdHolder.get());
});

// 任务2:未设置值却读取到残留值
pool.execute(() -> {
    System.out.println("任务2读取: " + taskIdHolder.get()); // 污染发生!
});

// 任务3:正确清理模式
pool.execute(() -> {
    try {
        taskIdHolder.set(3);
        System.out.println("任务3设置: " + taskIdHolder.get());
    } finally {
        taskIdHolder.remove();
    }
});

// 任务4:验证清理效果
pool.execute(() -> {
    System.out.println("任务4读取: " + taskIdHolder.get()); // 输出null
});