ThreadLocal:听说你很熟悉我?

62 阅读7分钟

基于 JDK1.8

ThreadLocal

ThreadLocal 可以用于创建线程本地变量,每个线程使用 ThreadLocal 都会拥有自己对该变量的副本,即每个线程都可以独立地改变其副本的值,而不会影响其他线程的副本。

数据结构

ThreadLocal 其实就是由内部实现的 ThreadLocalMap 组成的,ThreadLocalMap  是以 ThreadLocal弱引用作为 keyvalue 为代码中放入的值,每个线程在往 ThreadLocal 里放值的时候,都会往自己的 ThreadLocalMap 里存,读也是以 ThreadLocal 作为引用,在自己的 map 里找对应的 key ,从而实现了线程隔离。

image.png

ThreadLocalMap

数据结构

ThreadLocalMap 底层就是由 Entry 数组构成

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

image.png

ThreadLocalMap.set() 原理解析

源码

private void set(ThreadLocal<?> key, Object value) {
    // Entry数组
    Entry[] tab = table;
    int len = tab.length;
    // 获取当前key 的索引位置
    int i = key.threadLocalHashCode & (len-1);
    // 以当前 key 对应的 Entry 为起点,遍历 Entry 数组
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 当前 Entry 的 key 等于插入值的 key
        if (k == key) {
            e.value = value;
            return;
        }
        // 当前 Entry 的 key 为 null
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 插入元素
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 进行启发式清理工作,清除 key 为 null 的数据
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
  • keyHashCodeEntry 数组的长度减一相与获取 key 对应的索引位置

  • 根据索引位置获取 key 对应的 Entry,以该 Entry 为起点遍历 Entry 数组

    • 如果当前 Entrynull ,那么直接创建一个新的 Entry 插入
    • 如果当前的 Entry 不为 null
      • 如果当前 Entrykey 等于插入值的 key ,那么就会替换当前 Entryvalue ,然后结束方法。
      • 如果当前 Entrykeynull ,说明 Entry 是过期数据,执行 replaceStaleEntry 方法来替换过期的数据,然后结束方法。
      • 如果以上两种情况都不符合那么就会获取到下一个 Entry 继续判断
  • 增加 Entry 数组的长度

  • 调用 cleanSomeSlots 方法进行启发式清理工作,清理数组中  Entry 的 key 为 null 的数据,如果 cleanSomeSlots 方法未清理任何数据,且当前数组的长度超过了阈值,那么就会调用 rehash 方法进行扩容

ThreadLocalMap.replaceStaleEntry() 原理解析

源码

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // 待插入元素对应的索引位置
    int slotToExpunge = staleSlot;
    // 从待插入元素对应的索引位置开始向前迭代,
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    // 从待插入元素的索引位置 staleSlot 开始向后迭代
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // key 相同
        if (k == key) {
            e.value = value;
            // 互换位置
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // 清除过期 Entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        // key 为null
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // 未找到 key相同的Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    // 清理过期数据
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
  • 从插入值的索引位置 staleSlot 开始向前迭代,找到其他过期数据,然后更新过期数据的起始位置 slotToExpunge,直到 Entrynull
  • 从待插入元素的索引位置 staleSlot 开始向后迭代
    • 如果找到了 key 相同的 Entry  ,则将当前 Entry 的值设置为我们插入的值,然后将当前 Entry 的位置与待插入元素对应的 Entry 位置互换,接着就根据第 1 步获取到的 slotToExpunge 开始过期 Entry 的处理,最后退出方法。
    • 如果当前 Entrykeynull,且过期数据的起始位置 slotToExpunge 位于 staleSlot ,那么更新 slotToExpunge 为当前 Entry 的索引位置。
  • 如果在循环中找不到 key 相同的 Entry,就会创建新的Entry替 换 table[stableSlot] 的位置
  • 最后进行过期数据的清理

ThreadLocalMap.expungeStaleEntry() 原理解析

int staleSlot:空 slot 的起始位置

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // staleSlot 即为过期 Entry 的起始位置
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    // 从起始位置开始向后遍历
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // key 为null
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        // key 不为 null
        } else {
            // 判断位置是否偏离
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // 位置偏离重新计算位置
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
  • 将起始位置的 Entry 设置为 null ,并减小数组中元素的个数。
  • 从起始位置开始向后遍历 Entry 数组:
    • 如果遇到 keynullEntry ,那么就把当前的 Entry 设置为 null ,并且减小数组中元素的个数,然后继续往后探测。
    • 如果碰到 key 不为空的 Entry (记作 e ),那么就会重新计算当前 key 对应的索引位置(记作 h ),如果与原先的索引位置(也就是当前的 i 值)发生偏离,那么就会将 i 对应的 Entry 设置为 null ,然后重新计算 e 的位置。
    • 如果碰到空的 slot  则结束清理,返回空 slot 的位置。

ThreadLocalMap.cleanSomeSlots() 原理解析

int i:当前 Entry 的位置
int n:在插入元素时调用,这个参数代表元素的个数;在 replaceStaleEntry 中调用,这个参数代表数组的长度 。

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
  • 从当前 Entry 开始向后遍历,遍历 log2(n)  个元素
  • 遇到 key 为null 的Entry 则调用 expungeStaleEntry 进行清理

ThreadLocalMap 扩容机制

在set方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值就会执行rehash()

rehash 方法

private void rehash() {
    expungeStaleEntries();

    // 数组中的元素个数是否大于 threshold * 3/4
    if (size >= threshold - threshold / 4)
        resize();
}
  • 首先会调用 expungeStaleEntries 方法对 Entry 数组进行过期数据的清理
  • 然后判断当前数组中的元素个数是否大于 threshold * 3/4 ,如果大于那么就会调用 resize 方法进行扩容

resize 方法

private void resize() {
    Entry[] oldTab = table;
    // 旧容量
    int oldLen = oldTab.length;
    // 新容量
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 重新计算hash
                int h = k.threadLocalHashCode & (newLen - 1);
                // hash 冲突则往后查找
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    // 重新计算扩容阈值
    setThreshold(newLen);
    size = count;
    table = newTab;
}
  • 扩容后的 Entry 数组的长度变为旧容量的 2
  • 然后去遍历旧的 Entry 数组,重新计算 hash 位置,然后放到新的 Entry 数组中,如果出现 hash 冲突则往后寻找最近的 Entrynull 的槽位,遍历完成之后,旧数组中所有的 Entry 数据都已经放入到新的 Entry 数组中。最后重新计算数组的扩容阈值。

ThreadLocalMap.getEntry() 原理解析

ThreadLocal<?> key :当前key

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
        return getEntryAfterMiss(key, i, e);
}

根据 keyhash 获取对应的 Entry,如果当前 Entrykey 等于查找的 key 那就返回当前 Entry,否则就调用 getEntryAfterMiss 方法。

ThreadLocalMap.getEntryAfterMiss() 原理解析

key 在其直接哈希槽中找不到时使用

ThreadLocal<?> key : ThreadLocal 对象
int i : ThreadLocal 对应的 hash 值
Entry e : ThreadLocal 对应的 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)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
  • 如果当前 key 对应的 Entry 不为空
    • 判断当前 Entrykey 是否等于获取的 key ,是的话那么就返回当前的 Entry
    • 如果当前 Entrykeynull 那么就会调用 expungeStaleEntry 方法来清理,接着继续往后迭代。
  • 如果当前 key 对应的 Entry 为空就直接返回 null