ThreadLocal-源码篇

526 阅读8分钟

前言

上一篇我们已经粗略讲解了 ThreadLocal 的常见问题和原理。接下来要逐字逐句地去解析源码了。上篇还有很多没有讲清楚的这次也慢慢补上。至于应用我周末会把这几天写的文章好好在斟酌一下,感觉最近有点追求数量而没有追求质量。毕竟我有点心急。好了,废话不多说,进入源码学习吧!

源码分析

其实 ThreadLocal 源码没什么东西,都是调用 ThreadLocalMap 的。

hash 相关

ThreadLocalMap 元素的哈希值

// threadLocal 会存到 Thread.threadLocals 的 map中,
// 该 hashcode 经过计算后可以确定要存放的桶位
private final int threadLocalHashCode = nextHashCode();

/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
* 创建ThreadLocal对象时会使用到,每创建一个threadLocal对象 就会使用nextHashCode 
* 分配一个hash值给这个对象。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
 /* 每创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。
 * 这个值很特殊,它是斐波那契数也叫黄金分割数。hash增量为这个数字,带来的好处就是 hash分布非常均匀。
 */
private static final int HASH_INCREMENT = 0x61c88647;
/**
 * Returns the next hash code.
 * 创建新的ThreadLocal对象时  会给当前对象分配一个hash,使用这个方法。
 */
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

get 方法

接下来解析 get 方法,解析 get 分为两块,一块是 ThreadLocal 的 get 方法,一块是 ThreadLocalMap 的 get 方法。而 ThreadLocal.get() 调用的是 ThreadLocalMap.getEntry()。那么首先看 ThreadLocal.get()。

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程 Thread 对象的 threadLocals map 引用
    ThreadLocalMap map = getMap(t);
    // 条件成立:说明当前线程已经拥有自己的 ThreadLocalMap 对象了
    if (map != null) {
        // key:当前 threadLocal 对象
        // 调用 map.getEntry() 方法,获取 threadLocalMap 中该 threadLocal 关联的 entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 条件成立:说明当前线程初始化过
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    /*
     * 执行到这里有几种情况?
     * 1. 当前线程对应的 threadLocalMap 是空
     * 2. 当前线程与当前 threadLocal 对象没有生成过相关联的 线程局部变量
     */
    return setInitialValue();
}

// 如果当前 threadLocal 没有 value,初始化一个
// 如果当前线程没有 threadLocalMap 的话,还会初始化创建map。
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将 thread 于 value 关联
        map.set(this, value);
    else
        //执行到这里,说明 当前线程内部还未初始化 threadLocalMap ,这里调用createMap 给当前线程创建map
        createMap(t, value);
    return value;
}
// 大部分会重写的
protected T initialValue() {
    return null;
}
// 就是给当前线程的 threadLocals 初始化
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

这里记住一个小点,ThreadLocal 第一次初始化的时候是 get 方法。

接下来看一下核心源码 getEntry()。ThreadLocal 对象 get() 操作其实是由 ThreadLocalMap.getEntry 代理完成的。当 要查找的桶位上的元素为空或者 桶位的 key 不是我们要查找元素的 key,那么我们会调用 getEntryAfterMiss 方法向后继续查找。为什么会这么设计呢?因为 ThreadLocalMap 发生冲突时不会产生链表,如果当前散列表 slot 上有元素,就会向后查找直到找到可以使用的位置。

private Entry getEntry(ThreadLocal<?> key) {
  int i = key.threadLocalHashCode & (table.length - 1);
  Entry e = table[i];
  if (e != null && e.get() == key)
    return e;
  else
    //1. e == null
    //2. e.key != 要查找的 key
    /**
     * getEntryAfterMiss 方法会继续向当前桶位后面继续搜索  e.key = key  的entry
     */
    return getEntryAfterMiss(key, i, e);
}
/**
  * key:threadlocal 对象
  * i: key 计算出来的 index
  * e: table[index] 中的 entry
  */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
		// 循环处理当前元素,碰到 slot == null 情况,搜索结束
    while (e != null) {
      // 获取 slot 中 entry 对象 的key
      ThreadLocal<?> k = e.get();
      if (k == key)
        // 找到了
        return e;
      // 条件成立:说明当前 slot 中的 entry#key 关联的 threadLocal 对象已经被 GC 回收了
      // 因为 key 是弱引用 key = entry.get = null
      if (k == null)
        // 做一次探测式过期数据回收
        expungeStaleEntry(i);
      else
        // 更新 index,继续向后搜索
        i = nextIndex(i, len);
      // 获取下一个 slot 中的 entry 
      e = tab[i];
    }
    // 没找到
    return null;
}
//参数 staleSlot table[staleSlot] 就是一个过期数据,以这个元素开始,向后查找过期数据
//碰到 slot == null 的情况结束
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // help GC
    tab[staleSlot].value = null;
    // 这里直接置空
    tab[staleSlot] = null;
    // 干掉一个元素 -1
    size--;

    // 清理完get 过程中的过期数据还不算完,它还会继续向后查找,如果继续碰倒过期数据也会清理掉
    // 知道碰倒 slot == null 为止
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;//for 直到碰倒 slot == null 结束
         i = nextIndex(i, len)) {
      ThreadLocal<?> k = e.get();
      // 条件成立:说明 k 表示的 theadlocal 对象已经被 GC 回收了..当前 entry 属于脏数据
      if (k == null) {
        e.value = null;
        tab[i] = null;
        size--;
      } else {
        // 执行到这里说明当前 slot 中对应的 entry 是非过期数据
        // 前面有可能清理掉了几个过期数据
        // 当前元素有可能碰倒 hash 冲突了,往后偏移存储了,那么重新计算位置,让位置更靠近正确位置
        // 这样查询效率更高
        
        // 重新计算 entry 对应的 index
        int h = k.threadLocalHashCode & (len - 1);
        // 如果成立,说明当前元素发生过冲突并向后偏移了
        if (h != i) {
          // 将当前位置置空
          tab[i] = null;

          // Unlike Knuth 6.4 Algorithm R, we must scan until
          // null because multiple entries could have been stale.
          // 查找正确位置
          while (tab[h] != null)
            h = nextIndex(h, len);
          // 将当前元素放到距离正确位置更近的地方或正确位置
          tab[h] = e;
        }
      }
    }
    return i;
}

set 方法

修改当前线程与当前 threadLocal 对象相关联的 线程局部变量。(源码很简单,几乎在上面都看过。)

public void set(T value) {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}

我们着重看一下 map 的 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)]) {
      // 获取当前元素的 key
      ThreadLocal<?> k = e.get();
      // 如果要插入的 key 和 桶位的 key 相等,说明这是一个替换操作
      if (k == key) {
        e.value = value;
        return;
      }
      // 条件成立:说明遇到了过期数据
      if (k == null) {
        // 替换逻辑
        replaceStaleEntry(key, value, i);
        return;
      }
    }
    //执行到这里,碰到了 slot == null
    // 直接插入就可以了
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
}

这里单独分开是因为替换过期的方法 replaceStaleEntry() 很难。它大致的原理是如果该 slot 上有过期数据,那么我们就强制将值设置到该桶位。

// staleSlot: 上层方法 set 方法,迭代查询时 发现这个 slot 是一个过期 entry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // 表示开始探测式清理过期数据的开始下标。默认从 staleSlot 开始
    int slotToExpunge = staleSlot;
    // 向前迭代,找到没有过期的数据过期数据,循环遇到 null 结束
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
      // 找到过期数据,探测式清理数据开始下标更新为 i
      if (e.get() == null)
        slotToExpunge = i;

    // 以当前 staleSlot 开始向后查找,直到找到 null 位置
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
      ThreadLocal<?> k = e.get();
      //条件成立,说明替换逻辑
      // 这里是什么意思呢?就是如果我计算好某个下标比如 4 是应该插入的位置,
      // 但是 4 号位是过期数据。那么我们向后查找的过程中发现 6 号位与 4 号位 key 一样
      // 那么可能 6 号插入时发生冲突了。所以我们要把要插入的元素放到它应该存在的位置。
      // 即 4 号和 6 号互换位置。
      if (k == key) {
        e.value = value;
	// 交换位置
        tab[i] = tab[staleSlot];
        tab[staleSlot] = e;

        if (slotToExpunge == staleSlot)
          slotToExpunge = i;
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        return;
      }


      if (k == null && slotToExpunge == staleSlot)
        slotToExpunge = i;
    }
    // 向后查找过程中并未发现 k == key 的 entry,说明当前 set 操作是一个添加逻辑
    // 直接新数据添加到 table[staleSlot]对应的 slot 中
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    // 条件成立:除了当前 staleSlot 以外,还发现其他过期 slot,所以要开启清理数据逻辑
    if (slotToExpunge != staleSlot)
      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}


rehash

rehash 也是一个难点,代表了 ThreadLocalMap 的扩容逻辑。ThreadLocalMap 的扩容不是到达阈值之后直接进行扩容,而是到达阈值后去尝试清理数据。如果当前散列表中元素还是超过 四分之三的阈值,那么再进行扩容。扩大到源来容量的两倍。


private void rehash() {
    // 这个方法执行完后,当前散列表内的所有过期数据,都会被清理
    expungeStaleEntries();

    // 清理完之后,散列表内的 entry 数量仍达到了 threshould * 3/4 真正触发扩容
    if (size >= threshold - threshold / 4)
      resize();
} 
  
  
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 扩容后新表的大小
    int newLen = oldLen * 2;
    // 创建出扩容后的散列表
    Entry[] newTab = new Entry[newLen];
    //新表中的元素数量
    int count = 0;
    // 遍历老表,迁移数据到新表
    for (int j = 0; j < oldLen; ++j) {
      Entry e = oldTab[j];
      // 条件成立:老表中指定位置有数据
      if (e != null) {
        ThreadLocal<?> k = e.get();
        // 说明老表中当前位置是一个过期数据
        if (k == null) {
          e.value = null; // Help the GC
        } else {
          // 执行到这里说明这里是非过期数据,需要迁移到扩容后的表、
          int h = k.threadLocalHashCode & (newLen - 1);
          // 循环找到一个距离 h 最近的一个可以使用的 slot
          while (newTab[h] != null)
            h = nextIndex(h, newLen);
          // 将数据存放在新的合适的位置
          newTab[h] = e;
          count++;
        }
      }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

结语

整个 ThreadLocal 系列就算告一段落,周末会在第一篇文字补点应用。原理内容什么的都将清楚了,感觉再面试中遇到不会被问倒吧。希望对您有帮助。

2021年03月17日22:41:14

站在巨人肩膀

B 站小刘讲源码