ThreadLocal源码解析01

先看下set的流程图

img

ThreadLocal 简介

ThreadLocal 在面试中经常提到,关于ThreadLocal使用不当造成OOM以及在特殊场景下,通过ThreadLocal可以轻松实现一些看起来复杂的功能,都说明值得花时间研究其原理。

ThreadLocal 不是 Thread,是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,每个线程存储的是主线程变量的副本,子线程操作的是对副本进行操作,不影响其他子线程的中的数据,所以一般说,ThreadLocal不保证线程的安全,只保证线程的隔离。

举个例子,如果ThreadLocal保存保存的是一个静态变量,副本都是静态变量自己,这样就又会出现线程安全问题。

ThreadLocal 注意事项

  1. ThreadLocal类封装了getMap()、set()、get()、remove()4个核心方法
  2. 通过getMap()获取每个子线程Thread持有自己的ThreadLocalMap实例, 因此它们是不存在并发竞争的。可以理解为每个线程有自己的变量副本。
  3. ThreadLocalMap中Entry[]数组存储数据,初始化长度16,后续每次都是2倍扩容。主线程中定义了几个变量,Entry[]中就有几个key。
  4. Entry的key是对ThreadLocal的弱引用,当抛弃掉ThreadLocal对象时,垃圾收集器会忽略这个key的引用而清理掉ThreadLocal对象, 防止了内存泄漏。

源码解读

看源码要有入口我们先从初始化开始

ThreadLocal 初始化方式

/**
    * 定义一个ThreadLocal
    */
   private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

   public static void main(String[] args) {
       //设置变量的值
       threadLocal.set(1);
   }

我们发现ThreadLocal的set方法是设置值的,他为什么能是变量的副本呢我们进入set方法

ThreadLocal.set 方法

//设置此线程局部变量的当前线程副本
public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        //如果map为空就创建一个显得Map参数是当前线程和我们需要设置的值
        createMap(t, value);
    }

我看看set方法 很简单,获取当前线程,根据当前线程获取ThreadLocalMap,有的话就set没有就创建

我们可以这样理解 ThreadLocalMap 和当前线程有关,我们进去看下

ThreadLocal.getMap方法

//从当前线程获取  ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

好吧到这里 确定了ThreadLocalMap 实在当前线程中存储了,这也解释了为什么是变量的副本,调用ThreadLocal的set方法实际上是将值放进当前的线程中了,每个线程中的值是不一样的。

我们需要进入Thread内部看源码了

//再Thread 中定义的ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;

我们发现ThreadLocalMap 实在Thread类中定义的

到这里ThreadLocal.getMap方法解析完了,我们需要看createMap方法了。

ThreadLocal.createMap方法

set 方法是获取线程内部的ThreadLocalMap,初始化肯定为空,就需要调用createMap了

//创建ThreadLocalMap
   void createMap(Thread t, T firstValue) {
       /**
        * this 就是ThreadLocal 本身
        * 
        * firstValue 就是我们需要保存的值
        */
       t.threadLocals = new ThreadLocalMap(this, firstValue);
   }

我看看这段代码,很有意思,ThreadLocalMap的key是threadLocal本身,value则是我们需要设置的值,这里就出现一个问题,key是相同的,如果一个ThreadLocal有多个值肯定会被覆盖,所以可以确定,ThreadLocalMap是用来处理一个线程中存在多个ThreadLocal的问题,value肯定有更细化的对象存储,我们进去看看ThreadLocalMap的构造方法。

ThreadLocalMap的构造方法

/**
 *构建一个最初包含(firstkey,firstvalue)的新的 ThreadLocalMap。
 *因为Thread中的 ThreadLocalMaps是懒加载构造的,所以我们只创建
 *
 * @param firstKey ThreadLocal 本身
 * @param firstValue  需要存放的第一个值
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //创建一个Entry数组的表,初始化大小为INITIAL_CAPACITY
    table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
    //通过与运算将threadLocalHashCode映射到一个数组下标
    //他比取模或者求余速度快性能高
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //将取模后的值映射到对应的Entry数组中的某个位置
    table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
    //因为第一次调用设置size为1
    size = 1;
    //计算下次需要扩容的值
    setThreshold(INITIAL_CAPACITY);
}

//setThreshold方法是计算扩容下次扩容的阈值的
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

我们来看下Entry

ThreadLocalMap.Entry

img

//ThreadLocal作为key进行软引用
static class Entry extends WeakReference<ThreadLocal<?>> {
          //与ThreadLocal相绑定的值
            Object value;
     Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
      }
}

这里使用了WeakReference 软引用

WeakReference: 当一个对象仅仅被weak reference(软引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。

也就是如果当前线程的ThreadLocal 被销毁后,因为当前线程引用了ThreadLocalMap,所以当前线程和entry还是强引用,因为ThreadLocal在entry是软引用,所以垃圾回收key(ThreadLocal)会被销毁,entry中的value没有被销毁,但是没有key造成无法访问,这就造成了内存泄漏,ThreadLocal为了防止内存泄漏我们会在后面详细的说。

img

整体结构

我们回顾上面介绍的内容我们看下ThreadLocal整体结构的图解

img

ThreadLocalMap.set方法

我们上面介绍了getMap和createMap方法,我们来看看map.set方法

线性探测算法

ThreadLocalMap使用线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

先看一下线性探测相关的代码,从中也可以看出来table实际是一个环:

/**java
    * 获取环形数组的下一个索引
    */
   private static int nextIndex(int i, int len) {
       return ((i + 1 < len) ? i + 1 : 0);
   }

   /**
    * 获取环形数组的上一个索引
    */
   private static int prevIndex(int i, int len) {
       return ((i - 1 >= 0) ? i - 1 : len - 1);
   }
ThreadLocalMap的set()
/**
 * 将Value设置进对应的ThreadLocal 的key 中
 *
 * @param key
 * @param value
 */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    /**
     * 不使用 get的快速路径 set一般是替换方式
     * 快速路径一般会失败
     */
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    //获取table的长度
    int len = tab.length;
    //计算需要映射的table下标
    int i = key.threadLocalHashCode & (len - 1);
    /**
     * 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,
     * 就使用nextIndex()获取下一个(上面提到到线性探测法)。
     */
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        //获取当前的ThreadLocal
        ThreadLocal<?> k = e.get();
        //如果entry中的ThreadLocal(k) 和 传进来的 ThreadLocal(key)是同一个
        if (k == key) {
            //将 e.value替换为新的value
            e.value = value;
            return;
        }
        /**
         * table[i]上的key为空,说明被回收了(上面的弱引用中提到过)。
         * 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
         */
        if (k == null) {
            //对key为空的entry进行重新赋值替换
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //找到为空的插入位置,插入值,在为空的位置插入需要对size进行加1操作
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++size;
    /**
     * cleanSomeSlots用于清除那些e.get()==null,也就是table[index] != null && table[index].get()==null
     * 之前提到过,这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
     * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()
     */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

大致分析上面都已经标注出来了,需要注意的是Entry对象是继承是WeakReference也就是一个弱引用是会被回收的,所以对应 的key值可能是为null的。存放对象之后是需要判断数组中存储对象的个数是否超过了设定的临界值threshold的大小,如果超过了需要扩容,并且还要重新计算扩容后所有对象的位置。扩容的方法是rehash()

replaceStaleEntry 替换无效的key
/**
    * 替换无效entry
    */
   private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                  int staleSlot) {
       ThreadLocal.ThreadLocalMap.Entry[] tab = table;
       int len = tab.length;
       ThreadLocal.ThreadLocalMap.Entry e;

       /**
        * 根据传入的无效entry的位置(staleSlot),向前扫描
        * 一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
        * 直到找到一个无效entry,或者扫描完也没找到
        */
       int slotToExpunge = staleSlot;//之后用于清理的起点
       for (int i = prevIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = prevIndex(i, len))
           if (e.get() == null)
               slotToExpunge = i;

       /**
        * 向后扫描一段连续的entry
        */
       for (int i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
           ThreadLocal<?> k = e.get();

           /**
            * 如果找到了key,将其与传入的无效entry替换,也就是与table[staleSlot]进行替换
            */
           if (k == key) {
               e.value = value;

               tab[i] = tab[staleSlot];
               tab[staleSlot] = e;

               //如果向前查找没有找到无效entry,则更新slotToExpunge为当前值i
               if (slotToExpunge == staleSlot)
                   slotToExpunge = i;
               cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
               return;
           }

           /**
            * 如果向前查找没有找到无效entry,并且当前向后扫描的entry无效,则更新slotToExpunge为当前值i
            */
           if (k == null && slotToExpunge == staleSlot)
               slotToExpunge = i;
       }

       /**
        * 如果没有找到key,也就是说key之前不存在table中
        * 就直接最开始的无效entry——tab[staleSlot]上直接新增即可
        */
       tab[staleSlot].value = null;
       tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

       /**
        * slotToExpunge != staleSlot,说明存在其他的无效entry需要进行清理。
        */
       if (slotToExpunge != staleSlot)
           cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
   }
expungeStaleEntry 连续段清除
/**
     * 连续段清除
     * 根据传入的staleSlot,清理对应的无效entry——table[staleSlot],
     * 并且根据当前传入的staleSlot,向后扫描一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
     * 对可能存在hash冲突的entry进行rehash,并且清理遇到的无效entry.
     *
     * @param staleSlot key为null,需要无效entry所在的table中的索引
     * @return 返回下一个为空的solt的索引。
     */
    private int expungeStaleEntry(int staleSlot) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;

        // 清理无效entry,置空
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        //size减1,置空后table的被使用量减1
        size--;

        ThreadLocal.ThreadLocalMap.Entry e;
        int i;
        /**
         * 从staleSlot开始向后扫描一段连续的entry
         */
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            //如果遇到key为null,表示无效entry,进行清理.
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                //如果key不为null,计算索引
                int h = k.threadLocalHashCode & (len - 1);
                /**
                 * 计算出来的索引——h,与其现在所在位置的索引——i不一致,置空当前的table[i]
                 * 从h开始向后线性探测到第一个空的slot,把当前的entry挪过去。
                 */
                if (h != i) {
                    tab[i] = null;
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        //下一个为空的solt的索引。
        return i;
    }
cleanSomeSlots 清理脏数据
/**
    * 启发式的扫描清除,扫描次数由传入的参数n决定
    *
    * @param i 从i向后开始扫描(不包括i,因为索引为i的Slot肯定为null)
    *
    * @param n 控制扫描次数,正常情况下为 log2(n) ,
    * 如果找到了无效entry,会将n重置为table的长度len,进行段清除。
    *
    * map.set()点用的时候传入的是元素个数,replaceStaleEntry()调用的时候传入的是table的长度len
    *
    * @return true if any stale entries have been removed.
    */
   private boolean cleanSomeSlots(int i, int n) {
       boolean removed = false;
       ThreadLocal.ThreadLocalMap.Entry[] tab = table;
       int len = tab.length;
       do {
           i = nextIndex(i, len);
           ThreadLocal.ThreadLocalMap.Entry e = tab[i];
           if (e != null && e.get() == null) {
               //重置n为len
               n = len;
               removed = true;
               //依然调用expungeStaleEntry来进行无效entry的清除
               i = expungeStaleEntry(i);
           }
       } while ( (n >>>= 1) != 0);//无符号的右移动,可以用于控制扫描次数在log2(n)
       return removed;
   }

n的用途

主要用于扫描控制(scan control),从while中是通过n来进行条件判断的说明n就是用来控制扫描趟数(循环次数)的。在扫描过程中,如果没有遇到脏entry就整个扫描过程持续log2(n)次,log2(n)的得来是因为n >>>= 1,每次n右移一位相当于n除以2。如果在扫描过程中遇到脏entry的话就会令n为当前hash表的长度(n=len),再扫描log2(n)趟,注意此时n增加无非就是多增加了循环次数从而通过nextIndex往后搜索的范围扩大,示意图如下
img

rehash 重新整理

rehash 方法分两步

1、先是删除过期的对象:expungeStaleEntries();

2、如果存储对象个数大于临界值的3/4,扩容

/**
 * 刷新ThreadLocal
 */
private void rehash() {
   //全清理过期的数据
    expungeStaleEntries();
   /**
     * threshold = 2/3 * len
     * 所以threshold - threshold / 4 = 1en/2
     * 这里主要是因为上面做了一次全清理所以size减小,需要进行判断。
     * 判断的时候把阈值调低了。
     */
    if (size >= threshold - threshold / 4)
        //扩容
        resize();
}
expungeStaleEntries 全清理无效的entry
/**
   * 全清理,清理所有无效entry
   */
  private void expungeStaleEntries() {
      ThreadLocal.ThreadLocalMap.Entry[] tab = table;
      int len = tab.length;
      //遍历整个table
      for (int j = 0; j < len; j++) {
          ThreadLocal.ThreadLocalMap.Entry e = tab[j];
         // Entry存在且key不存在 就是 threadLoca已被GC回收
          if (e != null && e.get() == null)
              //使用连续段清理
              expungeStaleEntry(j);
      }
  }

删除数组中过时的Entry对象。有些小伙伴可能会有些疑问什么是过时的Entry?为什么会过时?其实这个在前面说过,Entry是弱引用会被回收。这个方法中判断的删除条件是,Entry对象不为空并且key值为空。可见expungStaleEntry(j) 方法就是删除指定索引的Entry对象。

resize扩容方法
/**
 * 扩容,扩大为原来的2倍(这样保证了长度为2的冥)
 */
private void resize() {
    //将table 赋值为old table
    ThreadLocal.ThreadLocalMap.Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //扩容是原来的二倍
    int newLen = oldLen * 2;
    //创建一个新的table
    ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
    int count = 0;

    //遍历old table
    for (int j = 0; j < oldLen; ++j) {
        //获取当前遍历到的entry
        ThreadLocal.ThreadLocalMap.Entry e = oldTab[j];
        //如果存在entry
        if (e != null) {
            //获取key
            ThreadLocal<?> k = e.get();
            //虽然做过一次清理,但在扩容的时候可能会又存在key==null的情况。
            if (k == null) {
                //将value置为空,让GC进行回收
                e.value = null; // Help the GC
            } else {
                //重新计算下标
                int h = k.threadLocalHashCode & (newLen - 1);
                //同样适用线性探测来设置值,如果发生hahs冲突找到向后找到最近的一个空位
                while (newTab[h] != null) {
                    h = nextIndex(h, newLen);
                }
                //将entry 放进计算出的table对应的下标数组中
                newTab[h] = e;
                count++;
            }
        }
    }
    //重新设置下次扩容的阈值
    setThreshold(newLen);
    //赋值threadLocal 的size
    size = count;
    //将新的table赋值为 table对象
    table = newTab;
}

先是创建一个是原来容量两倍的Entry[]数组,在遍历原来的数组,将key值为空的Entry对象的value置为空方便GC回收,key不为空的Entry对象先根据key的hashcode计算需要存放的位置存入新的数组中,存储结束后别忘了更新临界值。

到这里整个set方法的过程也完结了