ThreadLocal

1. ThreadLocal 基本概念

ThreadLocal 提供了线程局部变量,这些变量与普通变量不同,每个访问该变量的线程都有自己独立的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,用于将状态(如用户 ID 或事务 ID)与线程关联起来。

  • 线程隔离:每个线程都有自己独立的变量副本
  • 无锁操作:不需要同步机制,因为每个线程操作自己的副本
  • 生命周期:与线程绑定,线程结束时自动清理

2. Thread 和 ThreadLocal 关系

既然 ThreadLocal 是线程内部的局部变量,肯定和线程是有关系的,怎么扯商关系的?

Thread 里就维护了一个 ThreadLocal.ThreadLocalMap 类型的变量
这个变量没有修饰符,访问权限默认是包私有,Thread 和 ThreadLocal 是同一个包

// Thread 类
public class Thread implements Runnable {
  
    // ThreadLocal 的底层存储结构
    ThreadLocal.ThreadLocalMap threadLocals = null;
  
    // InheritableThreadLocal 的底层存储结构
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

}

3. ThreadLocal 类结构

public class ThreadLocal<T> {
  
  	// 构造
    public ThreadLocal() { }
  
  	// 匿名内部类方式设置初始值
    protected T initialValue() { return null; }
  
  	// 也是设置初始值
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
  
  	// 获取值
    public T get() { }
  
  	// 设置值
    public void set(T value) { }
  
  	// 清空
    public void remove() {}
  
  	// 内部类(就是 Thread.threadLocals,是一个 map)
    static class ThreadLocalMap { }
}

initialValue()withInitial() 方法都是用来设置初值的,对比下两者区别:

// initialValue 方式
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "默认值";
    }
};

// withInitial 方式(JDK8提供的,这个方法更简洁是主流方式)
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");

内部类 ThreadLocalMap 是一个定制化的哈希表,内部维护了一个 Entry 类(存储键值对),是弱引用类型

static class ThreadLocalMap {
  
  	// 弱引用,后面细说
    static class Entry extends WeakReference<ThreadLocal<?>> { }
}

4. ThreadLocal 实现原理

set() 方法实现

public void set(T value) {
    Thread t = Thread.currentThread(); // 当前线程
    ThreadLocalMap map = getMap(t); // 拿到线程的 threadLocals 变量(没修饰符,包私有)
    if (map != null) // 如果 threadLocals 不为空,就往里添加
        map.set(this, value); // this
    else // 如果 threadLocals 为空就初始化
        createMap(t, value);
}

当 threadLocals 为空就初始化,并把值添加进去,源码如下:

void createMap(Thread t, T firstValue) {
  	// 调用构造方法
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// 初始化逻辑,哈希表基本都是这一套流程
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  	// 1. 初始化Entry数组(哈希表),初始容量为INITIAL_CAPACITY(默认16)
    table = new Entry[INITIAL_CAPACITY];
  	// 2. 计算数组下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  	// 3. 在计算出的位置创建新的Entry节点,存储键值对
    table[i] = new Entry(firstKey, firstValue);
  	// 4. 设置当前元素数量为1(因为只插入了第一个元素)
    size = 1;
  	// 5. 设置扩容阈值(通常是容量的2/3)
    setThreshold(INITIAL_CAPACITY);
}

当 threadLocals 不为空,就调用 map.set(this, value); 来添加值,需要先理解下这个 this
因为一个线程中可以有多个 ThreadLolcal 实例,所以 key 是 this

ThreadLocal<String> userTL = new ThreadLocal<>();
ThreadLocal<Integer> ageTL = new ThreadLocal<>();

// 同一线程内存储两个独立变量
userTL.set("Alice");  // this是userTL
ageTL.set(25);        // this是ageTL

再来看 map.set(this, value) 原理,源码如下:

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table; // ThreadLocalMap 的 数组
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 计算下标

  	// 源码是:Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]
  	// 转成 while 循环,效果一致
    Entry e = tab[i];  // 1. 从初始位置开始检查,获取当前位置的 Entry

    while (e != null) { // 2. e != null,说明 i 下标位置有元素
        
        ThreadLocal<?> k = e.get(); // 检查当前Entry的key

        if (k == key) { // 情况1:找到相同的key,直接覆盖
            e.value = value;
            return;
        }

        if (k == null) { // 情况2:entry 不为空,但是 key 为空?弱引用,是可能出现的
            replaceStaleEntry(key, value, i); // 替换元素
            return;
        }

        
        i = nextIndex(i, len); // 情况3:hash 冲突:找下一个位置(开放寻址法)
        e = tab[i]; // 修改 e,继续循环

    }

  	// 3. 找到的下标位置 i 为空,直接插入元素
    tab[i] = new Entry(key, value);
    int sz = ++size;
  
  	// 尝试清理一些槽位,如果不需要扩容且size达到阈值,则执行rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

get() 方法实现

看了 set 的实现,再看 get 就会比较简单了,源码如下:

public T get() {
    Thread t = Thread.currentThread(); // 当前线程
    ThreadLocalMap map = getMap(t); // 拿到线程的 threadLocals 变量
    if (map != null) { // 如果不为空,取出 Entry,如果 Entry 也不为空,返回value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue(); // 如果 threadLocals 是空,说明还未初始化,设置默认值并返回
}

5. ThreadLocal 内存泄漏问题

原因分析

ThreadLocalMap 的 key 是弱引用,但 value 是强引用,所以发生 GC 时 key 一定被回收,但是 value 不会被回收(因为线程还存在)

当线程终止时,value 才会在 GC 时被回收,问题就出现在这里,线程可能一直不会终止(线程池中的线程

解决方案

每次使用完 ThreadLocal 后调用 remove() 方法清理

6. 使用示例

多个 ThreadLocal

通过这个示例也会明白为什么 set 源码中要传 this 了,因为线程内部可能会有多个 ThreadLocal 实例

  1. 定义 ThreadLocal 容器

    public class RequestContextHolder {
      
        // 第一个ThreadLocal:用户ID
        private static final ThreadLocal<Long> CURRENT_USER_ID = new ThreadLocal<>();
        
        // 第二个ThreadLocal:请求ID
        private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
        
        public static void setUserId(Long userId) {
            CURRENT_USER_ID.set(userId);
        }
        
        public static Long getUserId() {
            return CURRENT_USER_ID.get();
        }
        
        public static void setRequestId(String requestId) {
            REQUEST_ID.set(requestId);
        }
        
        public static String getRequestId() {
            return REQUEST_ID.get();
        }
        
        public static void clear() {
            CURRENT_USER_ID.remove();
            REQUEST_ID.remove();
        }
    }
    
  2. AOP 切面(网关,过滤器)

    @Aspect
    @Component
    public class RequestContextAspect {
        
        // 拦截所有Controller方法
        @Pointcut("execution(* com.example..controller.*.*(..))")
        public void controllerMethods() {}
        
        // 前置通知,设置值到 ThreadLocal 中
        @Before("controllerMethods()")
        public void beforeController(JoinPoint joinPoint) {
            // 模拟从请求头获取用户ID,设置到 CURRENT_USER_ID 中
            String userIdHeader = request.getHeader("X-User-Id");
            RequestContextHolder.setUserId(Long.parseLong(userIdHeader));
            
            // 生成请求ID,设置到 REQUEST_ID 中
            RequestContextHolder.setRequestId(UUID.randomUUID().toString());
        }
        
        // 后置通知,清除 ThreadLocal
        @After("controllerMethods()")
        public void afterController() {
            RequestContextHolder.clear();
        }
    }
    
  3. Controller、Service 中使用

    AOP 切的是 controller 中的方法,会使这个请求链路调用结束才清除 ThreadLocal

    @RestController
    public class UserController {
        
        @GetMapping("/me")
        public void getCurrentUser() {
          
            // 直接从ThreadLocal获取上下文
            Long userId = RequestContextHolder.getUserId();
            String requestId = RequestContextHolder.getRequestId();
    
            UserProfile profile = userService.getProfile(userId);
        }
    }
    
    
    @Service
    public class UserService {
        
        public UserProfile getProfile(Long userId) {
            // Service 方法也算在请求链路中,所以也能获取到
            String requestId = RequestContextHolder.getRequestId();
        }
    }
    

SimpleDateFormat 线程安全

SimpleDateFormat 不是线程安全的,可以使用 ThreadLocal 为每个线程创建独立实例。

public class DateFormatUtil {
    private static final ThreadLocal<SimpleDateFormat> dateFormat = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    
    public static String format(Date date) {
        return dateFormat.get().format(date);
    }
}

7. 总结

  1. 使用 private static final 修饰 ThreadLocal 变量
  2. 使用后必须调用 remove() 方法清理
  3. 考虑使用 ThreadLocal.withInitial() 方法初始化
  4. 避免在大量短生命周期的线程中使用 ThreadLocal
posted @ 2025-04-26 17:13  CyrusHuang  阅读(12)  评论(0)    收藏  举报