前言
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。我也写过一篇该类库的源码解析:
https://blog.csdn.net/jiangxiayouyu/article/details/105605116
源码解析
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 0 private 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); } // 只用于 createInheritedMap private 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); } }
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。