【生吃源码】ThreadLocal是什么

49 阅读5分钟

ThreadLocal是什么

在ThreadLocal类的注释中是这样描述:

这个类提供线程局部变量。这些变量与普通变量的不同之处在于,访问一个变量的每个线程(通过它的get或set方法)都有它自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,希望将状态与线程(例如,用户ID或事务ID)相关联。例如,下面的类生成每个线程的本地唯一标识符。线程的id是在它第一次调用ThreadId.get()时分配的,并且在后续调用中保持不变。

  import java.util.concurrent.atomic.AtomicInteger;
 
  public class ThreadId {
      // Atomic integer containing the next thread ID to be assigned
      private static final AtomicInteger nextId = new AtomicInteger(0);
 
      // Thread local variable containing each thread's ID
      private static final ThreadLocal<Integer> threadId =
          new ThreadLocal<Integer>() {
              @Override protected Integer initialValue() {
                  return nextId.getAndIncrement();
          }
      };
 
      // Returns the current thread's unique ID, assigning it if necessary
      public static int get() {
          return threadId.get();
      }
  }
  

只要线程是活的,并且ThreadLocal实例是可访问的,每个线程都持有对线程局部变量副本的隐式引用;在线程消失后,它的线程本地实例的所有副本都将被垃圾收集(除非存在对这些副本的其他引用)。

ThreadLocal可以认为就是跟随线程的一个map,可以在当前线程上下文中共享变量数据

ThreadLocal的实现

ThreadLocal提供了两个子类

image.png

SuppliedThreadLocal

其中SuppliedThreadLocal扩展了ThreadLocal,给定了不一样的初始方法(可以自定义初始值)

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {  

    private final Supplier<? extends T> supplier;  

    SuppliedThreadLocal(Supplier<? extends T> supplier) {  
        this.supplier = Objects.requireNonNull(supplier);  
    }  

    @Override  
    protected T initialValue() {  
        return supplier.get();  
    }  
}

ThreadLocalMap

ThreadLocalMap作为ThreadLocal的存储数据的对象其接口是一个弱引用key对象的table

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {  
        Object value;  
        Entry(ThreadLocal<?> k, Object v) {  
            super(k);  
            value = v;  
        }  
    }
    
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    private int size = 0;
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {  
        table = new Entry[INITIAL_CAPACITY];  
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  
        table[i] = new Entry(firstKey, firstValue);  
        size = 1;  
        setThreshold(INITIAL_CAPACITY);  
    }
    ……
}

弱引用对象 jdk内是这样描述的弱引用对象,它不阻止它们的引用被设置为可终结的、被终结的,然后被回收。弱引用最常用于实现规范化映射。假设垃圾收集器在某个时间点确定一个对象是弱可及的。此时,它将自动清除对该对象的所有弱引用,以及对通过强引用和软引用链可访问该对象的任何其他弱可及对象的所有弱引用。同时,它将所有以前弱可及的对象声明为可终结的。与此同时,或者在稍后的时间,它将使那些注册在引用队列中的新清除的弱引用进入队列。 就是弱引用对象在gc的时候会被回收

ThreadLocalMap储存在Thread对象的threadLocals上

Thread.java
/* ThreadLocal values pertaining to this thread. This map is maintained  by the ThreadLocal class. 
*/  
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap的set\get还有remove方法都是通过hash算法计算ThreadLocal<?> key的具体index位置后直接对Entry数组进行操作


ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;  
}

void createMap(Thread t, T firstValue) {  
    //new一个ThreadLocalMap对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);  
}

/**
* 下面代码中的this是指当前的ThreadLocal对象
*/

public T get() {  
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);  
    if (map != null) {
        //获取entry
        ThreadLocalMap.Entry e = map.getEntry(this);  
        if (e != null) {  
            @SuppressWarnings("unchecked")  
            //返回entry的value
            T result = (T)e.value;  
            return result;  
        }  
    }  
    return setInitialValue();  
}

//初始化方法
private T setInitialValue() {  
    T value = initialValue();  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null)  
        map.set(this, value);  
    else  
        //ThreadLocalMap不存在就初始化一下到当前的Thread对象上
        createMap(t, value);  
    return value;  
}

public void remove() {  
    ThreadLocalMap m = getMap(Thread.currentThread());  
    if (m != null)  
        //执行删除
        m.remove(this);  
}

//具体的操作数据方法,以remove为例
private void remove(ThreadLocal<?> key) { 
    //ThreadLocalMap的entry数组table
    Entry[] tab = table;  
    //table长度
    int len = tab.length;
    //计算index
    int i = key.threadLocalHashCode & (len-1);  
    //遍历table
    for (Entry e = tab[i];  
    e != null;  
    e = tab[i = nextIndex(i, len)]) {  
        if (e.get() == key) {
            //清除此引用对象  value的引用 = null  协助gc 
            e.clear();  
            //重新计算hash并清理key==null的entry
            expungeStaleEntry(i);  
            return;  
        }  
    }  
}

//ThreadLocalMap.set()
private void set(ThreadLocal<?> key, Object value) {  
    Entry[] tab = table;  
    int len = tab.length;  
    int i = key.threadLocalHashCode & (len-1);  

    for (Entry e = tab[i];  
    e != null;  
    e = tab[i = nextIndex(i, len)]) {  
        ThreadLocal<?> k = e.get();  

        if (k == key) {  
            e.value = value;  
            return;  
        }  

        if (k == null) {
            //删除包含该过期条目的“运行”中的所有过期条目
            replaceStaleEntry(key, value, i);  
            return;  
        }  
    }  

    tab[i] = new Entry(key, value);  
    int sz = ++size;  
    if (!cleanSomeSlots(i, sz) && sz >= threshold)  
        rehash();  
}

ThreadLocal可能会引发的问题

内存溢出

因为对ThreadLocalMap的Entry对象是弱引用ThreadLocal,key在被gc的时候依旧会被回收,value为强引用未被回收,大量堆积从而导致内存溢出(ps:为什么不采用强引用,不清理的时候直接不回收threadlocal对象和threadlocalMap,更快内存溢出,才用弱引用,下次set和get还有remove的时候都会去对key==null即threadlocal被回收的entry进行清理)

数据异常

在使用线程池的场景下(比如基于tomcat的springboot、MyBatis分页插件pageHelper),因为线程使用完成后未清理threadlocal且当前线程是base线程直接回归池中,线程复用导致了threadlocalMap的复用从而导致数据不是预期数据

ThreadLocal的应用案例

微服务调用的traceId

在feign作为远程调用中间件下,可以再服务的前置处理切面中将请求的headers中的traceId取出后放置到thread对象中,在feign下一个服务的时候通过feign提供的拦截器处理从threadLocal中取出后放置到请求headers中交由下一个服务处理,这里还可以携带一些服务调用链路下的通用信息,如用户信息等

线程变量

线程内上下文共用对象,比如日期转换(SimpleDateFormat)、随机器(Random)、业务id等

注意事项

使用结束后请务必对threadLoacl对象进行清理,如spring项目可在后置拦截器处理,避免其他不遵守规范的开发人员乱用导致问题,避免出现上文中提及的问题