面试又被问懵了吗?不如把ThreadLocal拆开了揉碎看看

作者 : 开心源码 本文共11437个字,预计阅读时间需要29分钟 发布时间: 2022-05-14 共92人阅读

前言

1.为什么用 ThreadLocal?

所谓并发,就是有限资源需要应对远超资源的访问。处理问题的方法,要么添加资源应对访问;要么添加资源的利用率。 所以,相信这年头做开发的多多少少,都会那么几个“线程二三招”、“用锁五六式”。 那所带来的就是多线程访问下的并发安全问题。 共享变量的访问域跨越了原始的单线程,进入了千家万户的线程眼里。谁都可以用,谁都可以改,那不就打起来了吗? 因而,防止并发问题的最好办法,就是不要多线程访问(这科技水平倒退二十年~)。ThreadLocal 顾名思义,将一个变量限制为“线程封闭”:对象只被一个线程持有、访问、修改。

2.那究竟什么是 ThreadLocal?

ThreadLocal 假如做到线程封闭,那诚然是独木难支。它必然携手 Thread 为广大 Javaer 带来福音。 ThreadLocal 自己不是存储者,它只是 Thread 的搬运工。独有变量必然是存在 Thread 中的。一般项目中多定义多个 ThreadLocal,那相应的 Thread 必然也需要存储那么多独有变量。 既然处理了线程之间的访问干扰,那一个线程的访问干扰自然就不在话下了。Thread 维护了一个 ThreadLocalMap,以“key-value”的形式存储了独有变量;以 ThreadLocal 实例为 key,精准获取。

3.ThreadLocal 需要考虑哪些问题?

假如线程死亡了,那 ThreadLocalMap、ThreadLocal 及独有变量都会被销毁。

但是现在避免线程的重复创立与销毁,线程使用完都是放回线程池。而假如没有手动移除 ThreadLocalMap 的元素,即便当前线程退出,ThreadLocal 已不被线程方法栈持有,也仍然无法被回收,从而造成内存泄漏。 所以 ThreadLocalMap.Entry 的 key(也就是 ThreadLocal)实际是弱引用。当没有其余强引用时,只需发生 GC,就会被回收,相当于这个时候 key 为 null。

这又产生了一个问题,key 被回收了, entry 和 value 可还是强引用呢,怎样办? ThreadLocalMap 已经考虑了这种情况,再调用 set()、get()、remove() 方法的时候,会清除掉 key 为 null 的记录。 所以人家设计是没有问题的,假如发生内存泄漏都是用的不对。 建议使用完 ThreadLocal方法后,最好手动调用remove()方法。

4.ThreadLocal 还需要考虑哪些问题?

随着业务场景的复杂化,变量的线程封闭诚然处理了访问的问题,但是也给线程传递带来了难度。 线程之间的协作,带来了变量在两个线程之间安全传递的需要。需要人为解决这种传递,需要三个步骤:

  • 线程 1 取出变量;
  • 线程 1 安全传递变量、ThreadLocal(其实一般选择共享)给线程 2,当心逃逸。
  • 线程 2 放到当前线程的 ThreadLocal。 这个步骤是通用的,只需存在使用 hreadLocal并且需要线程传递时,必然少不了这三步。 JDK 为我们提供了“线程 2 是线程 1 创立出来时,独有变量传递给线程 2”的处理方法:InheritableThreadLocal,Thread 中也有专门为其服务的 ThreadLocalMap。

那我们明白,在线程池化的世面下,不会经常存在创立的场景,更多的是与已有线程的协作。 各家公司,其实也会为相关业务的 ThreadLocal 自研类库,去做到传递。 市面上处理通用场景的线程传递的类库就是 TransmittableThreadLocal。

源码解析

Thread

public Class Thread implements Runnable {    //与此线程有关的 ThreadLocal 值。由 ThreadLocal 类维护    ThreadLocal.ThreadLocalMap threadLocals = null;    // 与此线程有关的 InheritableThreadLocal 值。由 InheritableThreadLocal 类维护    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocalMap 是 ThreadLocal 的内部类,是定制的 Map 实现。 初始值都为 null,只有当第一次调用对应ThreadLocal的 get 或者 set 时,才会初始化。

ThreadLcoal

ThreadLcoal 只有一个默认的无参构造函数。实际的初始化逻辑,都在第一次调用 get 或者 set 时。

get()

因为是相似懒加载的形式,所以 get 中涉及到ThreadLocalMap的创立以及初始值设置。

public T get() {    Thread t = Thread.currentThread();    // 获取线程的 map, 为啥要抽取方法呢?就是为了扩展之前提到的 InheritableThreadLocal    ThreadLocalMap map = getMap(t);    if (map != null) {    // 已经 set 过    ThreadLocalMap.Entry e = map.getEntry(this);    if (e != null) {    @SuppressWarnings("unchecked")    // 走到这里没有 Entry 的情况:remove 以后    T result = (T)e.value;    return result;    }    }    // 未 set 过的第一次 get (map == null)    // 或者 set 过, 但是 remove 了 (map != null && e == null)    return setInitialValue();}private T setInitialValue() {    // 获取指定初始值, 默认是 null    // 可以通过 withInitial(Supplier<? extends S> supplier) 工厂方法来创立指定初始化值的 ThreadLocal    T value = initialValue();    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null) {        map.set(this, value);    } else {        // ThreadLocalMap 未初始化        createMap(t, value);    }    if (this instanceof TerminatingThreadLocal) {        // 解决一个特殊子类的逻辑        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);    }    return value;}   
void createMap(Thread t, T firstValue) {    t.threadLocals = new ThreadLocalMap(this, firstValue);}ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}

指定初始值的工厂构造方法

// 假如以下情况下的第一次 get, 判断 map 的 entry 为 null下// 1.从未 set 过;// 2.remove 过后protected T initialValue() {    return null;}

默认初始值是 null。 可以通过以下工厂方法,获取一个指定初始化逻辑的 ThreadLocal。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {    return new SuppliedThreadLocal<>(supplier);}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();    }}

set() & remove()

set() 有点像 setInitialValue(),只不过一个是初始值,一个是指定值。

两个方法其实本身都简单,主要依赖于 ThreadLocalMap的操作。

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

ThreadLocalMap

  • 这个类是 ThreadLocal 的内部类,是包私有的。
  • key 的 hashcode 是自己设置的增长值。
  • key 是 WeakReference 的。

Entry

可以看到 key 就是 ThreadLocal,一定不为空,但也是弱引用的。

也就是说,当 key 为 null 时,说明 ThreadLocal 已经被回收了,对应的 Entry 就应该被清理了。

static class Entry extends WeakReference<ThreadLocal<?>> {    /** The value associated with this ThreadLocal. */    Object value;    Entry(ThreadLocal<?> k, Object v) {        super(k);        value = v;    }}

预设值

  • 初始容量为 16,扩容翻倍。所以容量肯定为 2 的 n 次幂。
  • 负载因子是 2/3。
  • 初始化时,应该是第一次设置值,或者来源于 ThreadLocalMap。所以算得上饿汉式加载。
private static final int INITIAL_CAPACITY = 16;private Entry[] table;private int size = 0;private int threshold; // Default to 0private void setThreshold(int len) {    threshold = len * 2 / 3;}

构造函数

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);}ThreadLocal {    /**     * 人为设置的 hash code 分布. 对于在相同线程中使用连续构造的 ThreadLocal, 可以有效避免冲突.     * 由于是可以预见的场景, 仅在 ThreadLocalMap 中使用.     */    private final int threadLocalHashCode = nextHashCode();    /**     * The next hash code to be given out. Updated atomically. Starts at     * zero.     */    private static AtomicInteger nextHashCode =  new AtomicInteger();    private static final int HASH_INCREMENT = 0x61c88647;    private static int nextHashCode() {        return nextHashCode.getAndAdd(HASH_INCREMENT);    }}

每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal 对象,hash 值就添加一个固定的大小 0x61c88647。这个东西比较讲究,有兴趣可以自行研究一下。

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;         // 开放定址法: 索引位置 + 1         e = tab[i = nextIndex(i, len)]) {        ThreadLocal<?> k = e.get();        if (k == key) {            e.value = value;            return;        }        if (k == null) {            // key 为空, 说明 对应的 ThreadLocal 已经回收了.            // 可以复用当前位置.            // 有两种情况:1\. entry 存在, 在这个过时位置的后面. 所以需要置换到这个位置            // 2.不存在, 直接放到这个位置            replaceStaleEntry(key, value, i);            // 由于是替换, 所以size 要么不变,要么减少。            return;        }    }    // 没找到已存在的, 也没找到可以替换的过时. 则直接新建    tab[i] = new Entry(key, value);    int sz = ++size;    if (!cleanSomeSlots(i, sz) && sz >= threshold)        // 假如没有清理过时 entry, 并且超过阈值. 则进行先尝试缩小,不行则扩容        rehash();}

类中定义了两个方法用于开放定址法的查找:增量为 1。

private static int prevIndex(int i, int len) {    return ((i - 1 >= 0) ? i - 1 : len - 1);}private static int nextIndex(int i, int len) {    return ((i + 1 < len) ? i + 1 : 0);}

replaceStaleEntry()

replaceStaleEntry() 比较复杂。一是需要清理过时 entry,二是开放定址法要保证所计算出的索引值后面的元素连续性。

所以,replaceStaleEntry() 会检查当前可替换位置的前后最近的两个空档之间所有的过时 entry。

其次,假如是 key 已存在过时位置的后面,那原有位置替换后会留出空档,需要后面的 entry 都往前挪一位(空档前的)。

private void replaceStaleEntry(ThreadLocal<?> key, Object value,                               int staleSlot) {    Entry[] tab = table;    int len = tab.length;    Entry e;    int slotToExpunge = staleSlot;    // 1.往前查找第一个空档后的最小过时    for (int i = prevIndex(staleSlot, len);         (e = tab[i]) != null;         i = prevIndex(i, len))        if (e.get() == null)            slotToExpunge = i;    // 往前查找第一个空档前的 key 或者 最大过时    for (int i = nextIndex(staleSlot, len);         (e = tab[i]) != null;         i = nextIndex(i, len)) {        ThreadLocal<?> k = e.get();        // 找到对应的 entry        if (k == key) {            e.value = value;            // 2.将key 与原位置的过时替换            tab[i] = tab[staleSlot];            tab[staleSlot] = e;            if (slotToExpunge == staleSlot)                // 3.假如前面都没有过时的话,那这个区间的第一个过时就是原来的staleSlot, 现在的 i                slotToExpunge = i;            // 4.清除过时, 挪移 entry            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);            return;        }        // 5.假如前面没有空槽, 且有新的过时, 则重新标记第一个过时.(由于staleSlot肯定会被替换成不过时的,到时候就不是第一个过时点了)        if (k == null && slotToExpunge == staleSlot)            slotToExpunge = i;    }    // 6.直接替换    tab[staleSlot].value = null;    tab[staleSlot] = new Entry(key, value);    // slotToExpunge == staleSlot, 说明当前区间只有这个过时, 已经被替换了, 所以不需要再进行清理    if (slotToExpunge != staleSlot)        // key 本不在, 且前或者后存在其余的过时        // 7.清除过时, 挪移 entry        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

5、7 因为是清除过时,后面再详细说明。

区间是当前过时位置staleSlot前后第一个空位所组成的范围,即下图两个空白格子之间。

我们根据区间的不同情况,做了图例说明。

key 存在:

key 不存在:

rehash()

当 set() 完,数量到达阈值,是先尝试能不能删掉少量过时的。假如删无可删,或者者删完之后达不到标准,则扩容。

注意的是,这个标准不是之前的 threshold,而是 3/4 threshold,避免滞后性。

private void rehash() {    // 对整个数组进行扫描,清除.    // 而不像替换那步, 只扫描区间    expungeStaleEntries();    // Use lower threshold for doubling to avoid hysteresis    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 (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);                while (newTab[h] != null)                    h = nextIndex(h, newLen);                newTab[h] = e;                count++;            }        }    }    setThreshold(newLen);    size = count;    table = newTab;}private void expungeStaleEntries() {    Entry[] tab = table;    int len = tab.length;    for (int j = 0; j < len; j++) {        Entry e = tab[j];        if (e != null && e.get() == null)            expungeStaleEntry(j);    }}

expungeStaleEntry()

从上面的分析可以看到,该方法应用在 replaceStaleEntry 和 expungeStaleEntries。

replaceStaleEntry是对区间进行解决, expungeStaleEntries是对全数组。所以expungeStaleEntry(int)就是上述解决的一个子集。这样了解下来,就是清除指定位置到下一个空位之间的过时 entry,包含指定位置:[index, indexOf(first null))。

  • index 肯定是一个过时元素的位置。

  • 既然过时的会被清理,那中间就会留出空位。开放定址法是要求连续的,所以重新计算索引来放置。

  • 注意:保留的 key 是重新计算索引, 而不是简单地往前挪一位。

  • 这是由于清理区间的过时,是在某个 key 与运算出的起始索引之前。

  • 而 key 恰好在这个索引上,简单往前挪一位,下次查找可能就找不到了。

  • 由于要求连续性地从头遍历到尾,一旦中间出现空位,就找不到了。

private int expungeStaleEntry(int staleSlot)     Entry[] tab = table;    int len = tab.length;    // expunge entry at staleSlot    // 明确当前位置肯定是过时的, 先直接清除掉    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();        if (k == null) {            // 清除过时            e.value = null;            tab[i] = null;            size--;        } else {            // 由于前面留出空档, 所以后面的元素都要重新计算索引, 以望填补空位            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;}

cleanSomeSlots()

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];        // 没扫到任何过时,共扫描 log2(n) 个槽;        if (e != null && e.get() == null) {            // 上述期间扫到过时,则将该区间遍历:            // 而后基于区间终点,重新扫描 log2(length);            // 假如扫到,重复上面;            // 假如一直重复,最终扫描了全数组。            n = len;            removed = true;            i = expungeStaleEntry(i);        }    } while ( (n >>>= 1) != 0);    return removed;}

cleanSomeSlots 一般在新添加一个元素或者删除另一个旧元素(不是 remove,而是 set 时恰好删掉另一个过时的后),进行扫描或者清理。

起始位置是一个元素不是过时的索引,是扫描完一个区间后的终点(空位)或者新添加元素的位置。

终点的话,由于使用的是对数扫描,是两个极端情况的平衡:

  • 没扫到任何过时,共扫描 log2(n) 个槽;

  • 上述期间扫到过时,则将该区间遍历;

  • 而后基于区间终点,重新扫描 log2(length);

  • 假如扫到,重复上面;

  • 假如一直重复,最终扫描了全数组。

get & remove

get:

  • 假如直接找到,则返回;
  • 假如没有,在开放定址法的增量下,遍历查找。而这个过程,还需要兼职清理区间内的过时(expungeStaleEntry(int))。

remove:

找到指定的 key, 清理完,同样兼职清理一下区间内的。

内存泄漏

经过上述的分析,因为 key 也就是 ThreadLocal 在 Entry 中是 WeakReference 的。

ThreadLocal 在没有外部强引用时,发生 GC 的话,ThreadLocalMap的弱引用将不会影响回收。

那相当于 Entry 中的 key = null,可是 Entry 和 Value 都是强引用,是无法跟随着 key 一起被销毁的。

想想 ThreadLocal 的作用,当 ThreadLocal 都被销毁了,那 key-value 的存储就没有意义了。

假如等到兼职任务去清理过时,也是存在时间差的,在 value 是大对象的时候,也是较为麻烦的。

所以建议

当使用完退出时,最好使用ThreadLocal.remove()方法将该变量主动移除。

InheritableThreadLocal

当线程 2 是从 线程 1 创立的时候,可以指定能否从线程 1 继承 ThreadLocal。当然,前提是线程 1 使用了可以被继承的 InheritableThreadLocal。

private Thread(ThreadGroup g, Runnable target, String name,               long stackSize, AccessControlContext acc,               boolean inheritThreadLocals) {    ...... 省略    Thread parent = currentThread();    // parent.inheritableThreadLocals 不为空, 要当前线程必需使用 InheritableThreadLocal     if (inheritThreadLocals && parent.inheritableThreadLocals != null)        this.inheritableThreadLocals =            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);    ...... 省略}

ThreadLocal :

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {    return new ThreadLocalMap(parentMap);}// 只用于 createInheritedMapprivate ThreadLocalMap(ThreadLocalMap parentMap) {    Entry[] parentTable = parentMap.table;    int len = parentTable.length;    setThreshold(len);    table = new Entry[len];    for (Entry e : parentTable) {        if (e != null) {            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();            if (key != null) {                // 从父类那计算子类值, 默认是一样的                Object value = key.childValue(e.value);                Entry c = new Entry(key, value);                int h = key.threadLocalHashCode & (len - 1);                while (table[h] != null)                    // hash 冲突解决方法是,开放定址法                    h = nextIndex(h, len);                table[h] = c;                size++;            }        }    }}

InheritableThreadLocal:

可以看到使用 InheritableThreadLocal,操作的 Thread的变量是不同于 ThreadLocal。

恰好对应了上面创立 Thread,继承父线程的 inheritableThreadLocals。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {    protected T childValue(T parentValue) {        // 从父类值计算子类值, 可以重写        return parentValue;    }    ThreadLocalMap getMap(Thread t) {        // 获取的 map 不同       return t.inheritableThreadLocals;    }    void createMap(Thread t, T firstValue) {        // 使用的 map 不同        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);    }}

作者:SaltyFishInJiang
链接:https://juejin.cn/post/6931230924549914637
来源:掘金

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 面试又被问懵了吗?不如把ThreadLocal拆开了揉碎看看

发表回复