ThreadLocal 如何实现线程间隔离,为什么 ThreadLocal 常常容易呈现内存溢出。带着这两个问题,在源码中找寻答案。
先从设置值开始,看 ThreadLocal.set()
如何实现的值保留。
public void set(T value) {Thread t = Thread.currentThread();
// 获取线程公有属性 threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {map.set(this, value);
} else {createMap(t, value);
}
}
-
threadLocals: 线程 Thread 对象外部属性,这个属性默认就是 null,当初看来就是由 ThreadLocal.set 进行初始化的了。
先不必管 ThreadLocalMap 如何实现,只有把他当作当成类型 Map 这类容器。一开始没有想明确线程应用变量不是一个 Object,而是一个容器。set 值时为什么不间接 ThreadLocal set val 间接给 Thread.threadLocals 而是将一个容器赋值。转念一想一个 Thread + 一个 ThreadLocal 只能保留一个 val,然而一个 Thread 能够对应多个 ThreadLocal,一个线程对象属性可能被多个 ThreadLocal 独特持有,差点被一个 Thread、一个 ThreadLocal 限度了思路。
先看下createMap
如何初始化的,value 如何保存起来。void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue); }
-
ThreadLocalMap
: ThreadLocal 一个外部类,名字看起来像 Map 实现类,其实实质并没有和 Map 有任何关系,也没有实现 Map 接口。外部应用 Entry(相似 Map key-value 对象) 数组存储数据,应用 Hash 算法计算下标。如果呈现 hash 抵触如何解决呢,这个低配 Entry 并没有链表或者红黑树这样黑科技。static class ThreadLocalMap { /** * 应用弱援用包装 ThreadLocal 作为 map Key * 在某些状况下 key 会被回收掉 */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) {super(k); value = v; } } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY]; // 应用 hashCode 计算下标 // 这里应用 hashCode 跟咱们一般对象不一样,通过自增长计算出来 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
-
WeakReference
: 弱援用的对象领有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具备弱援用的对象,不论以后内存空间足够与否,都会回收它的内存。
因而一旦呈现 gc ThreadLocalMap 的 key 就会被回收掉,从而导致 ThreadLocal 设置 value 不能被删除,对象积压过多导致内存溢出。当初第二个问题失去答案了因为 map key 会被 gc 开释,从而导致 value 不能被删除,所以应用实现后必须手动开释掉 val。private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 循环外面解决 hash 抵触状况 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get(); if (k == key) { // 相等间接笼罩 e.value = value; return; } if (k == null) { // key 曾经被 gc // 这个 i 不会变,只能向后找一位 // 这里会持续向后找,直到找到符合条件地位,并且将 key 被 gc value 也回收掉,// 这个只有在肯定条件下才失效 replaceStaleEntry(key, value, i); return; } } // 当 hash 抵触时,会始终向后找,直到有空地位 tab[i] = new Entry(key, value); int sz = ++size; // 在地位 i 前面搜查是否有 key 回收状况,则删除数组地位,返回 true // 当有删除,不须要判断扩容状况了,一个新增对应删除,容量都没有减少 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); // 扩容数组}
在呈现 hash 抵触时,只是将下标向后挪动,找到闲暇的地位。正如 set 办法正文上写,set 并不反对疾速 set、抵触了通过向后遍历数组找到空地位。
ThreadLocalMap 竟然还有检测 key 为空机制,并且删除数组中地位。这个机制要肯定状况能力触发,首先被回收 key 必须在新增 key 前面地位能力被发现。
看下ThreadLocal.get
如何取值public T get() {Thread t = Thread.currentThread(); // 调用 get 并不会初始化 threadLocals ThreadLocalMap map = getMap(t); if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) {@SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 没有取到值,会返回 null return setInitialValue();}
看下 ThreadLocalMap 外部如何将值返回的吧
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); } // 向后查找合乎要去 key private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table; int len = tab.length; while (e != null) { // 遇到 null 就停下来 ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) // k 曾经被 gc 了 // 在数组中删除这个地位,这样能够帮忙 value 回收了 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
这里 get 也不是一次性找到,会通过向后遍历匹配进去,这个以 HashMap 相比差距挺大的。插入、查找效率都在 N 之间。
结尾两个纳闷,当初都失去解决了。
不晓得大家是否和我好奇 ThreadLocalMap 的弱援用在应用时为什么不会被 gc 掉,导致传值进去而不能获取返回值呢,为什么要应用弱援用来包装 Key,如何权衡利弊的。
做一个小例子public void print(){WeakReference<Object> reference = getReference(); System.gc(); Object o = reference.get(); System.out.println(o); } public WeakReference<Object> getReference(){WeakReference<Object> reference = new WeakReference(new Object()); return reference; }
print 办法会输入 null,这个就是我纳闷的中央,为什么 ThreadLocal 不会呈现下面在应用弱援用中,执行 gc 后,援用被回收导致 key 为空,取不到值了。
在网上找到了答案,在应用一个例子验证下就会明确了@Test public void sidi() throws InterruptedException {getLocal(); System.gc(); TimeUnit.SECONDS.sleep(1); Thread thread = Thread.currentThread(); System.out.println(thread); // 在这里打断点,察看 thread 对象里的 ThreadLocalMap 数据 } private Local getLocal() {Local local = new Local(); System.out.println(local.get()); return local; } private static class Local {private ThreadLocal<String> local = ThreadLocal.withInitial(() -> "ssssssssss"); public String get() {return local.get(); } public void set(String str) {local.set(str); }
上面看下 debug 后果图
当初做一个小改变
为什么有一个返回值后,ThreadLocal 就不会被 gc 回收掉呢,其实跟这个跟强援用有关系的。以后根对象就是 local 对象,持有 ThreadLocal<String> local,尽管 local 是被弱援用包装有可能被 gc 的,然而同时被以后 local 的强援用关联,对象依然是可达的,不会被垃圾回收掉。以后办法没有持有 local 时,Local 外部 local 是没有任何对象援用它,强援用并没有作用到 ThreadLocal,执行 gc 时必定会删除弱援用。总结执行 gc 时,弱援用总会被垃圾回收掉的,然而如果弱援用的对象同时被强援用持有,强援用作用域会盖过弱援用,在强援用可达之前,对象是不会被回收的。所以平时咱们在应用 ThreadLocal 时不会放心弱援用被删除状况,咱们在操作 ThreadLocal 时会必须持有它的对象援用,强援用保障了在以后持有对象代码里对象不会被回收。
看下面经典图片,实线示意强援用,虚线示意弱援用。当线程栈持有 ThreadLocal,作为 Entry key 的它不会被 gc,当 ThreadLocalRef 援用生效时,ThreadLocal 就会在下次 gc 时被回收掉。在持有 ThreadLocal 对象援用链时,ThreadLocal 弱援用都不会被回收的。
最初一个问题,为什么 ThreadLocal 要作为弱援用作为 ThreadLocalMap 外部 key 存在呢。咱们晓得 ThreadLocal 作为多线程操作本身公有变量工具类,本事不持有任何线程的变量,只是封装具体实现不便使用者调用。ThreadLocal 自身对象生命周期很短的,用完就能够回收了。试想下 ThreadLocal 作为 Entry key 存在,线程援用链就会变成 Thread->ThreadLocalMap ->ThreadLocal, 在线程对象不会开释之前,ThreadLoca 对象不会被回收的。如果咱们不手动开释它,是不是就和 Entry key 被删除一样,对象太多可能导致内存泄露。ThreadLocal 作为一个工具类没必要和 Thread 一起绑定,设置一个弱援用包装它,能够在对象作用域隐没后有利于垃圾回收器回收它。即便没有手动开释设置 value,也不用太过放心内存透露这块,在新增值、数组扩容时都会查看有 key 被 gc 状况,对 val 开释。