java 并发--- ThreadLocal
Posted float123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 并发--- ThreadLocal相关的知识,希望对你有一定的参考价值。
文章部分图片来自参考资料
问题 :
- ThreadLocal 底层原理
- ThreadLocal 需要注意什么问题,造成问题的原因是什么,防护措施是什么
ThreadLocal 概述
ThreadLocal 线程本地变量 ,是一个工具,可以让多个线程保持一个变量的副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal 结构图里面看到有两个内部类,一个 SuppliedThreadLocal , 一个ThreadLocalMap 。下面用一张图来说明线程使用的示意图。可以看到每个Thread 有个 ThreadLocalMap ,然后里面由hash值分列的的数组 Entry[] 。Entry 数据结构就是图中淡绿色框内所示。所以 ThreadLocal 里面放的 value 应该是放在Thread里面的。
ThreadLocal 源码分析
ThreadLocal 下文简称 TL, TL最常见的方法就是 get 和 set 了。
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) 5 map.set(this, value); 6 else 7 createMap(t, value); 8 }
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9 return result; 10 } 11 } 12 return setInitialValue(); 13 }
1 ThreadLocalMap getMap(Thread t) { 2 return t.threadLocals; 3 }
1 ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到thread 内部中持有TL的内部类变量。我们来看一下 ThreadLocalMap, threadLocalMap 内部定义一个类,Entry 类。这是threadLocalMap 内的变量
ThreadLocalMap 类
1 static class ThreadLocalMap { 2 /** 3 * The initial capacity -- MUST be a power of two. 4 */ 5 private static final int INITIAL_CAPACITY = 16; 6 7 /** 8 * The table, resized as necessary. 9 * table.length MUST always be a power of two. 10 */ 11 private Entry[] table; 12 13 /** 14 * The number of entries in the table. 15 */ 16 private int size = 0; 17 18 /** 19 * The next size value at which to resize. 20 */ 21 private int threshold; // Default to 0 22 } 23
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 }
我们看到 TL 的set 方法实际就是调用了 ThreadLocalMap 的set 方法。
1 private void set(ThreadLocal<?> key, Object value) { 2 3 // We don\'t use a fast path as with get() because it is at 4 // least as common to use set() to create new entries as 5 // it is to replace existing ones, in which case, a fast 6 // path would fail more often than not. 7 8 Entry[] tab = table; 9 int len = tab.length; 10 int i = key.threadLocalHashCode & (len-1); 11 12 for (Entry e = tab[i]; 13 e != null; 14 e = tab[i = nextIndex(i, len)]) { 15 ThreadLocal<?> k = e.get(); 16 17 //找到相同的 key 18 if (k == key) { 19 e.value = value; 20 return; 21 } 22 23 //某个key失效 24 if (k == null) { 25 replaceStaleEntry(key, value, i); 26 return; 27 } 28 } 29 30 //走到这里必定是退出了循环,即是遇到空的 entry ,直接放在空的地方,检查是否需要扩容,重新 hash 31 tab[i] = new Entry(key, value); 32 int sz = ++size; 33 if (!cleanSomeSlots(i, sz) && sz >= threshold) 34 rehash(); 35 } 36 37 38 // 这个方法是替代某些失效的entry ,最终的值会放在 table[staleSlot] 39 // slotToExpunge 这个变量从名字上可以看出就是需要擦洗的 slot (指的是某个位置) 40 private void replaceStaleEntry(ThreadLocal<?> key, Object value, 41 int staleSlot) { 42 Entry[] tab = table; 43 int len = tab.length; 44 Entry e; 45 46 // Back up to check for prior stale entry in current run. 47 // We clean out whole runs at a time to avoid continual 48 // incremental rehashing due to garbage collector freeing 49 // up refs in bunches (i.e., whenever the collector runs). 50 // 向前找是否有失效节点,如果有做一下标记,即是为 slotToExpunge 赋值 51 int slotToExpunge = staleSlot; 52 for (int i = prevIndex(staleSlot, len); 53 (e = tab[i]) != null; 54 i = prevIndex(i, len)) 55 if (e.get() == null) 56 slotToExpunge = i; 57 58 // Find either the key or trailing null slot of run, whichever 59 // occurs first 60 // 向后寻找是否有相同的 key 61 for (int i = nextIndex(staleSlot, len); 62 (e = tab[i]) != null; 63 i = nextIndex(i, len)) { 64 ThreadLocal<?> k = e.get(); 65 66 // If we find key, then we need to swap it 67 // with the stale entry to maintain hash table order. 68 // The newly stale slot, or any other stale slot 69 // encountered above it, can then be sent to expungeStaleEntry 70 // to remove or rehash all of the other entries in run. 71 // 找到相同的值,交换位置到 tab[staleSlot] 72 if (k == key) { 73 e.value = value; 74 75 tab[i] = tab[staleSlot]; 76 tab[staleSlot] = e; 77 78 // Start expunge at preceding stale entry if it exists 79 // 擦洗失效值 80 if (slotToExpunge == staleSlot) 81 slotToExpunge = i; 82 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); 83 return; 84 } 85 86 // If we didn\'t find stale entry on backward scan, the 87 // first stale entry seen while scanning for key is the 88 // first still present in the run. 89 if (k == null && slotToExpunge == staleSlot) 90 slotToExpunge = i; 91 } 92 93 // If key not found, put new entry in stale slot 94 //找不到值会放在 tab[staleSlot] ,即原来失效值的位置上 95 tab[staleSlot].value = null; 96 tab[staleSlot] = new Entry(key, value); 97 98 // If there are any other stale entries in run, expunge them 99 // 擦洗失效值 100 if (slotToExpunge != staleSlot) 101 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); 102 } 103
可以看到我们在 set 的时候,TL内会检查是否存在失效值。也可以看到 ThreadLocalMap 的Hash 中解决冲突的方式只是简单的向下寻找空的位置,即线性探测,这样的效率比较低,所以建议 :
每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
下面看一下 get 方法,不难。
1 // ThreadLocalMap 2 private Entry getEntry(ThreadLocal<?> key) { 3 int i = key.threadLocalHashCode & (table.length - 1); 4 Entry e = table[i]; 5 if (e != null && e.get() == key) 6 return e; 7 else 8 return getEntryAfterMiss(key, i, e); 9 }
1 private Entry getEntry(ThreadLocal<?> key) { 2 int i = key.threadLocalHashCode & (table.length - 1); 3 Entry e = table[i]; 4 if (e != null && e.get() == key) 5 return e; 6 else 7 //获取的时候出现失效的entry 8 return getEntryAfterMiss(key, i, e); 9 } 10 11 12 // 往后找,失效的值擦洗掉,没有就返回 Null 13 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { 14 Entry[] tab = table; 15 int len = tab.length; 16 17 while (e != null) { 18 ThreadLocal<?> k = e.get(); 19 if (k == key) 20 return e; 21 if (k == null) 22 expungeStaleEntry(i); 23 else 24 i = nextIndex(i, len); 25 e = tab[i]; 26 } 27 return null; 28 }
使用ThreadLocal内存失效问题分析
为什么使用弱引用
我们来看一下weakReference 表示弱引用,java中有四种引用类型,强引用,弱引用,软引用,虚引用。
在Java语言中, 当一个对象o被创建时, 它被放在Heap里. 当GC运行的时候, 如果发现没有任何引用指向o, o就会被回收以腾出内存空间. 也就是说, 一个对象被回收, 必须满足两个条件:
-
没有任何引用指向它
-
GC被运行.
1 DemoA a=new DemoA(); 2 DemoB b=new DemoB(a);
假如有下面代码,如果我们增加一行代码来将a对象的引用设置为null,当一个对象不再被其他对象引用的时候,是会被GC回收的,但是对于这个场景来说,即时是a=null,也不可能被回收,因为DemoB依赖DemoA,这个时候是可能造成内存泄漏的。
1 DemoA a=new DemoA(); 2 DemoB b=new DemoB(a); 3 a=null;
通过弱引用,有两个方法可以避免这样的问题。
1 //方法1 2 DemoA a=new DemoA(); 3 DemoB b=new DemoB(a); 4 a=null; 5 b=null; 6 //方法2 7 DemoA a=new DemoA(); 8 WeakReference b=new WeakReference(a); 9 a=null; 10
对于方法2来说,DemoA只是被弱引用依赖,假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。
我们可以设想b就是ThreadLocal ,试想一下如果这里没有使用弱引用,意味着ThreadLocal的生命周期和线程是强绑定,只要线程没有销毁,那么ThreadLocal一直无法回收。而使用弱引用以后,当ThreadLocal被回收时,由于Entry的key是弱引用,不会影响ThreadLocal的回收防止内存泄漏,同时,在后续的源码分析中会看到,ThreadLocalMap本身的垃圾清理会用到这一个好处,方便对无效的Entry进行回收。
所以使用了弱引用的原因是为了防止 ThreadLocal 对象没有被正确回收而导致的内存泄漏。
ThreadLocalMap 使用了弱引用会导致内存泄漏
ThreadLocalMap下文简称 TLM ,是存在Thread 中的,那么它的生存周期必定和线程的生命周期一样长的。
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 }
ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。(出处)
我们从源码中也可以看到在 get 和 set 等方法都有检查失效值的操作,同时当我们使用TL时,某个线程不再需要某个值的时候应该调用 remove 方法,下面代码中 e.clear() 这一句实际是调用了弱引用的 clear 方法,实现对对象的回收。
1 private void remove(ThreadLocal<?> key) { 2 Entry[] tab = table; 3 int len = tab.length; 4 int i = key.threadLocalHashCode & (len-1); 5 for (Entry e = tab[i]; 6 e != null; 7 e = tab[i = nextIndex(i, len)]) { 8 if (e.get() == key) { 9 e.clear(); 10 expungeStaleEntry(i); 11 return; 12 } 13 } 14 }
1 /** 2 * Clears this reference object. Invoking this method will not cause this 3 * object to be enqueued. 4 * 5 * <p> This method is invoked only by Java code; when the garbage collector 6 * clears references it does so directly, without invoking this method. 7 */ 8 public void clear() { 9 this.referent = null; 10 }
其实我们从源码分析可以看到,ThreadLocalMap是做了防护措施的
首先从ThreadLocal的直接索引位置(通过
ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e
如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询
由上面的分析,我们知道了ThreadLocal 使用了弱引用后还是会导致内存泄漏,而内存泄漏的原因是 : ThreadLocalMap的生存周期和线程一样长,而不是使用弱引用导致的!!!
Entry 的 Hash 值
如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?
1 void createMap(Thread t, T firstValue) { 2 t.threadLocals = new ThreadLocalMap(this, firstValue); 3 }
1 static class ThreadLocalMap { 2 static class Entry extends WeakReference<ThreadLocal<?>> { 3 4 /** The value associated with this ThreadLocal. */ 5 Object value; 6 7 Entry(ThreadLocal<?> k, Object v) { 8 super(k); 9 value = v; 10 } 11 } 12 13 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 14 //构造一个Entry数组,并设置初始大小 15 table = new Entry[INITIAL_CAPACITY]; 16 //计算Entry数据下标 17 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 18 //将`firstValue`存入到指定的table下标中 19 table[i] = new Entry(firstKey, firstValue); 20 size = 1;//设置节点长度为1 21 setThreshold(INITIAL_CAPACITY); //设置扩容的阈值 22 } 23 //...省略部分代码 24 } 25 26
1 private final int threadLocalHashCode = nextHashCode(); 2 private static AtomicInteger nextHashCode = new AtomicInteger(); 3 private static final int HASH_INCREMENT = 0x61c88647; 4 5 private static int nextHashCode() { 6 return nextHashCode.getAndAdd(HASH_INCREMENT); 7 }
那为什么要使用到 0x61c88647 这个值呢? 我们首先要明白一点,散列的目的是使数据分布更加均匀。那么这个数字的使用必定会达到这个目的。
魔数0x61c88647的选取和斐波那契散列有关,0x61c88647对应的十进制为1640531527。而斐波那契散列的乘数可以用 (long)((1L<<31)*(Math.sqrt(5)-1)); 如果把这个值给转为带符号的int,则会得到-1640531527。也就是说(long)((1L<<31)*(Math.sqrt(5)-1));得到的结果就是1640531527,也就是魔数0x61c88647
建议
-
将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
-
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
- 在线程池中使用ThreadLocal ,有可能会出现数据混淆的情况,原因是数据没及时清理,线程放回线程池中又被拿出来使用。
总结
- ThreadLocal 使用 弱引用的原因是为了处理非常大和生命周期非常长的线程,为了防止 ThreadLocal回收导致的内存泄漏,
但是使用了弱引用也有可能导致内存泄漏,这次 ThreadLocal被回收了,但是value一直存活着,要是没有手动删除的话,
依旧会导致内存泄漏,所以ThreadLocalMap 的 get ,set 中都有防护措施---检查ThreadLocal 为空的 Entry ,然后删除掉该
Entry. - 在使用完ThreadLocal,记得调用remove 方法。
参考资料 :
- ThreadLocal-面试必问深度解析
- ThreadLocal内存泄漏真因探究
- 上面两篇文章必读!!
- JAVA高级架构 微信公众号的 “ThreadLocal的使用及原理分析”
- 从 ThreadLocal 的实现看散列算法
以上是关于java 并发--- ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章