java.lang.ThreadLocal类在平时的开发中很少用到,勾勾工作5年多一直没有用过。最近在学习spring源码时发现很多地方用到,并且这个类是面试高频题目,不明白为什么大厂喜欢考察这个类的知识,兴许他们的代码是勾勾等无法理解的高级吧。

不管怎么说,为了学习spring源码,勾勾也应该掌握这个类。

ThreadLocal介绍

学习一个新类的时候,勾勾喜欢先看JDK的官网介绍,凭借大学6级的水平勉强看懂。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via itsget or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

翻译:这个类用于提供线程局部变量(thread-local)。这些变量与普通变量不同,各个线程可以通过(get或者set方法)来获取自己初始化复制的变量副本。ThreadLocal的实例通常都是private static类型的字段,希望状态能和某一个线程(用户ID或者事务ID)相关联。

总结来说,ThreadLocal的作用:

  • 传递数据:在同一个线程中,不同组件之间通过ThreadLocal传递变量。
  • 线程隔离:在多线程并发环境下,每个线程的变量都是独立的,不会互相影响。

接下来,我们通过一个测试用例,先熟悉ThreadLocal的用法:

public class ThreadLocalTest {
//    private String state;
    //构造函数和重写initialValue方法
    ThreadLocal state = new ThreadLocal(){@Overrideprotected String initialValue(){return "init";
        }
    };//get方法private String getState() {return state.get();
    }//set方法private void setState(String newState) {this.state.set(newState);
    }//删除方法private void remove(){
        state.remove();
    }public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();for (int i = 0; i 5; i++) {new Thread(()->{
                test.setState(Thread.currentThread().getName()+"设置数据");
                System.out.println(Thread.currentThread().getName() + "-->" + test.getState());
                test.remove();
            },"thread-" + i).start();
        }
    }
}

勾勾先用了普通的String变量作为共享变量,结果有的线程取到了其他线程的数据,然后换成ThreadLocal类,所有的线程都只能取到自己设置的数据。

在上述用例中,我们用到了ThreadLocal的无参构造函数,initialValue和get、set、remove方法。

接下来我们学习ThreadLocal中的API。

  • T get() :Returns the value in the current thread's copy of this thread-local variable:返回当前线程中的绑定的变量值;
  • set(T value) :Sets the current thread's copy of this thread-local variable to the specified value:给当前线程中的变量副本设置值;
  • void remove() Removes the current thread's value for this thread-local variable:删除当前线程中的值。
  • protected T initialValue() :Returns the current thread's "initial value" for this thread-local variable:返回当前线程变量的初始值。

ThreadLocal类图介绍

springgboot中ThreadPoolExecutor 未执行 spring中threadlocal用法_threadlocal用法

ThreadLocal嵌套了内部类ThreadLocalMap,这个内部类的本质是Map,维护了key-value的数据关系。

ThreadLocalMap内部也维护了一个类Entry,此类继承WeakReference,Entry的key是ThreadLocal实例,是弱引用,value是传入的变量值,是一个强引用。

ThreadLocalMap是整个ThreadLocal类的核心,但是其引用不在ThreadLocal中而是在Thread类中,每个线程set值时都是向自己的ThreadLocalMap赋值,取得时候也是取得自己的,从而实现了线程间的隔离。

public class Thread implements Runnable {   
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //父线程和子线程可以共享变量副本的类
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

整个结构如下图:

springgboot中ThreadPoolExecutor 未执行 spring中threadlocal用法_构造函数_02



ThreadLocalMap介绍

static class ThreadLocalMap {
  //内部Entry类,继承了弱引用
        static class Entry extends WeakReference<ThreadLocal>> {
            /** 与ThreadLocal关联的value值*/
            Object value;
    //构造函数,key为ThreadLocal弱引用,value为强引用
            Entry(ThreadLocal> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * 初始化容量,必须是2的幂
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * entry[]数组,如果需要可以扩容,数组的长度也必须是2的幂         * 
         */
        private Entry[] table;

        /**
         * 数组中元素的个数
         */
        private int size = 0;

        /**
         * 扩容的阈值
         */
        private int threshold; // Default to 0
     
     //构造函数
      ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
          //创建初始容量大小的entry数组
            table = new Entry[INITIAL_CAPACITY];
          //根据第一个key的hashcode计算下标值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
          //将entry放入数组中
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
     //获取hashcode的方法
     private final int threadLocalHashCode = nextHashCode();
     //线程安全的原子类
     private static AtomicInteger nextHashCode =
         new AtomicInteger();
     //这个数与黄金分割线有关,可以使哈希码均匀分布在2的幂的数组中,可以尽量避免hash冲突。这里是解决hash冲突的关键代码
     private static final int HASH_INCREMENT = 0x61c88647;
     private static int nextHashCode() {
         return nextHashCode.getAndAdd(HASH_INCREMENT);
     }
     
     ...
 }

ThreadLocal内部维护了静态内部类ThreadLocalMap,其内部存储数据的关键结构是Entry数组,ThreadLocalMap内部没有链表结构,其解决hash冲突的主要方法一是使hash码尽量均匀分布另外一个就是采用线性探测法。

set方法源码

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果map不为空,则将value值与当前线程存入map中
    if (map != null)
        map.set(this, value);
    //如果map为空,则创建ThreadLocalMap,并将传入的value存入map中
    else
        createMap(t, value);
}
//获取ThreadLocalMap实例
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
//set
 private void set(ThreadLocal> key, Object value) {
     Entry[] tab = table;
     int len = tab.length;
     //根据key得hash值计算得到下标
     int i = key.threadLocalHashCode & (len-1);
     //根据线性探测法查找元素,即一直向后查找槽不为空的元素
     for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
         //ThreadLocal对应得key存在且相等则覆盖value值
         ThreadLocal> k = e.get();
         if (k == key) {
             e.value = value;
             return;
         }
         //key为null,但是entry还存在,说明之前得ThreadLocal对象已经被回收了
         if (k == null) {
             //用新元素替换旧元素
             replaceStaleEntry(key, value, i);
             return;
         }
     }
 //没有找到元素,则在下标为空的位置创建一个新的entry对象存入
     tab[i] = new Entry(key, value);
     int sz = ++size;
     //清除一些e.get == null的槽,如果元素数量大于负载因子则扩容
     if (!cleanSomeSlots(i, sz) && sz >= threshold)
         rehash();
 }

//根据构造函数创建ThreadLocalMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set方法的执行流程:

  • 获取当前运行的线程
  • 根据当前线程获取ThreadLocalMap实例map
  • 如果获取的map不为空,则调用ThreadLocalMap的set方法存入值。
  • 如果map为空,则新建当前线程的ThreadLocalMap实例,并将值存入map。

set方法流程图:

springgboot中ThreadPoolExecutor 未执行 spring中threadlocal用法_set方法_03

get方法源码

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程获取ThreadLocalMap引用
    ThreadLocalMap map = getMap(t);
    //如果map不为空
    if (map != null) {
        //则根据当前的ThreadLocal实例获取entry对象e
        ThreadLocalMap.Entry e = map.getEntry(this);
        //如果e存在
        if (e != null) {
            @SuppressWarnings("unchecked")
            //获取e对象的value值返回
            T result = (T)e.value;
            return result;
        }
    }
    //map不存在或者entry不存在时则设置初始化值并返回
    return setInitialValue();
}
// 获取当前线程得ThreadLocalMap引用
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private Entry getEntry(ThreadLocal> key) //根据key的hashCode值获取Entry[]数组的下标int i = key.threadLocalHashCode & (table.length - 1);
 //根据下标获取Entry[]数组的元素
 Entry e = table[i];
 // 如果元素不为空,且元素的key值与当前的参数key相等,则返回元素
 if (e != null && e.get() == key)
      return e;
 //如果元素为空,则再次检测Entry[]数组返回
 else
     return getEntryAfterMiss(key, i, e);
}
//当期线程获取找不到对应得entry时,将运行此分支方法
 private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
     Entry[] tab = table;
     int len = tab.length;

     while (e != null) {
         ThreadLocal> k = e.get();
         if (k == key)
             return e;
         if (k == null)
             //连续段清除,此方法多处用到,用于清理key为nul的槽
             expungeStaleEntry(i);
         else
             i = nextIndex(i, len);
         e = tab[i];
     }
     return null;
}

  private T setInitialValue() {
      //调用initialValue方法获取初始值,可以被子类重写,如果没有重写则默认为null
      T value = initialValue();
      //获取当前线程的ThreadLocalMap引用
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      //map不为空,则将初始value值map中
      if (map != null)
          map.set(this, value);
      //如果为空,则创建map,此处与set方法一样,只是设置的值为初始值
      else
          createMap(t, value);
      return value;
  }
//返回初始值
protected T initialValue() {
    return null;
}
// 创建当前先线程的ThreadLocalMap,并将value存入map中
 void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

get方法执行流程:

  • 获取当前运行的线程,并获取线程的ThreadLocalMap实例
  • 如果map不为空则以ThreadLocal实例为key通过getEntry方法获取实体对象
  • 如果实体对象不为空,则获取实体的value值返回。
  • 如果map为空或者实体对象为空,则设置初始化值并返回。

get方法的流程图:

springgboot中ThreadPoolExecutor 未执行 spring中threadlocal用法_数组_04

remove方法源码

public void remove() {
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    //如果map不为空,则删除当前线程的值
    if (m != null)
        m.remove(this);
}
  private void remove(ThreadLocal> key) {
      Entry[] tab = table;
      int len = tab.length;
      //计算当前线程ThreadLocal的下标值
      int i = key.threadLocalHashCode & (len-1);
      //遍历i下标位置及其下一个位置
      for (Entry e = tab[i];
           e != null;
           e = tab[i = nextIndex(i, len)]) {
          //如果e对象的key与要删除的key相等
          if (e.get() == key) {
              e.clear();
              //连续段清除
              expungeStaleEntry(i);
              return;
          }
      }
  }
 public void clear() {
     this.referent = null;
 }
  private int expungeStaleEntry(int staleSlot) {
      Entry[] tab = table;
      int len = tab.length;

      // 清理无效的entry,置为null
      tab[staleSlot].value = null;
      tab[staleSlot] = null;
      size--;

      // Rehash until we encounter null
      Entry e;
      int i;
      //连续向后扫描
      for (i = nextIndex(staleSlot, len);
           (e = tab[i]) != null;
           i = nextIndex(i, len)) {
          ThreadLocal> k = e.get();
          //如果entry的key为null,则将value置为null并清除
          if (k == null) {
              e.value = null;
              tab[i] = null;
              size--;
          } else {
              //如果entry的key不为null,则计算key的hash值并得到下标
              int h = k.threadLocalHashCode & (len - 1);
            //如果下标值与当前下标不相等,则清除当前下标的entry,从h位置向后探寻不为空的槽,把当前的entry挪过去
              if (h != i) {
                  tab[i] = null;
                  while (tab[h] != null)
                      h = nextIndex(h, len);
                  tab[h] = e;
              }
          }
      }
      return i;
  }

remove方法执行流程:

  • 获取当前线程的ThreadLocalMap
  • 如果map不为空,则执行clear方法将当前位置的槽置空。
  • 向后清理无效的entry

remove方法执行流程图:

springgboot中ThreadPoolExecutor 未执行 spring中threadlocal用法_构造函数_05

ThreadLocal内存泄漏

内存泄漏(Memory Leak):程序中已经动态分配的堆内存由于某种原因未释放或者无法释放时,造成系统内存的浪费。内存泄漏最终将会导致内存溢出,造成程序崩溃。

java对象的引用分为4级,级别从高到低分别为:强引用、软引用、弱引用、虚引用。

  • 强引用:我们平时创建的变量都是强引用(如果没有特殊的引用),只要对象活着,当JVM内存不足时就算OOM也不会回收强引用对象。
  • 软引用:如果内存空间充足则不会回收对象,如果空间不足就算是对象活着也会被回收。
  • 弱引用:不论空间是否充足,只要发生GC都会被回收。
  • 虚引用:任何时候都会被回收。

通过前面的Entry类的分析我们知道key是定死的ThreadLocal实例是弱引用,value是程序传入的值是强引用,我们先从内存结构上分析这种设计的问题为什么会导致内存泄漏。

springgboot中ThreadPoolExecutor 未执行 spring中threadlocal用法_数组_06

假设在程序代码中使用完ThreadLocal,那么ThreadLocal的引用被回收了。

由于ThreadLocalMap中只有ThreadLocal的弱引用,此时ThreadLocal也会被GC回收,此时Entry对象的key为null。

但是因为没有手动删除Entry,在CurrentThread运行的情况下,value不会被回收,但是value也不会被访问到,从而导致了内存泄漏。

为了防止内存泄漏,在ThreadLocal使用完的时候一定要手动remove。

今天就学到这里了,收工!