ThreadLocal 解决什么问题
ThreadLocal是为了解决对象不能被多线程共享访问的问题,通过 threadLocal.set() 方法将对象实例保存在每个线程自己所拥有的 threadLocalMap 中,这样的话每个线程都使用自己的对象实例,彼此不会影响从而达到了隔离的作用,这样就解决了对象在被共享访问时带来的线程安全问题先把 Thread, ThreadLocal, ThreadLocalMap 的关系捋一捋:
可以看到,在%20Thread%20中持有一个%20ThreadLocalMap%20,%20ThreadLocalMap%20又是由%20Entry%20来组成的,在%20Entry%20里面有%20ThreadLocal%20和%20value
ThreadLocal%20为什么会内存泄漏
首先是因为%20ThreadLocal%20是基于%20ThreadLocalMap%20实现的,其中%20ThreadLocalMap%20的%20Entry%20继承了%20WeakReference%20,而%20Entry%20对象中的%20key%20使用了%20WeakReference%20封装,也就是说,%20Entry%20中的%20key%20是一个弱引用类型,对于弱引用来说,它只能存活到下次%20GC%20之前。如果此时一个线程调用了%20ThreadLocalMap%20的%20set%20设置变量,当前的%20ThreadLocalMap%20就会新增一条记录,但由于发生了一次垃圾回收,这样就会造成一个结果:%20key%20值被回收掉了,但是%20value%20值还在内存中,而且如果线程一直存在的话,那么它的%20value%20值就会一直存在这样被垃圾回收掉的%20key%20就会一直存在一条引用链:%20Thread%20->%20ThreadLocalMap%20->%20Entry%20->%20Value%20:就是因为这条引用链的存在,就会导致如果%20Thread%20还在运行,那么%20Entry%20不会被回收,进而%20value%20也不会被回收掉,但是%20Entry%20里面的%20key%20值已经被回收掉了这只是一个线程,如果再来一个线程,又来一个线程…多了之后就会造成内存泄漏知道是怎么造成内存泄漏之后,接下来要做的事情就好说了,因为%20value%20值没有被回收掉所以才会导致内存泄露。所以在使用完%20key%20值之后,将%20value%20值通过%20remove%20方法%20remove%20掉,这样的话内存中就不会有%20value%20值了,也就防止了内存泄漏。
ThreadLocal%20是基于%20ThreadLocalMap%20实现的
在源码中能够看到下面这几行代码:
public%20class%20ThreadLocal<T>%20{%20%20%20%20static%20class%20ThreadLocalMap%20{%20%20%20%20%20%20%20%20static%20class%20Entry%20extends%20WeakReference<ThreadLocal<?>>%20{%20%20%20%20%20%20%20%20%20%20%20%20/**%20The%20value%20associated%20with%20this%20ThreadLocal.%20*/%20%20%20%20%20%20%20%20%20%20%20%20Object%20value;%20%20%20%20%20%20%20%20%20%20%20%20Entry(ThreadLocal<?>%20k,%20Object%20v)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20super(k);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20value%20=%20v;%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20}%20%20%20%20}}
代码中说的很清楚,在%20ThreadLocal%20内部维护着%20ThreadLocalMap%20,而它的%20Entry%20则继承自%20WeakReference%20的%20ThreadLocal<?>%20,其中%20Entry%20的%20k%20为%20ThreadLocal%20,%20v%20为%20Object%20,在调用%20super(k)%20时就将%20ThreadLocal%20实例包装成了一个%20WeakReference
| 引用类型 | 功能特点 |
|---|---|
| 强引用%20(%20Strong%20Reference%20) | 被强引用关联的对象永远不会被垃圾回收器回收掉 |
| 软引用(%20Soft%20Reference%20) | 软引用关联的对象,只有当系统将要发生内存溢出时,才会去回收软引用引用的对象 |
| 弱引用%20(%20Weak%20Reference%20) | 只被弱引用关联的对象,只要发生垃圾收集事件,就会被回收 |
| 虚引用%20(%20Phantom%20Reference%20) | 被虚引用关联的对象的唯一作用是能在这个对象被回收器回收时收到一个系统通知 |
从表格中应该能够看出来,弱引用的对象只要发生垃圾收集事件,就会被回收。所以弱引用的存活时间也就是下次%20GC%20之前了。为什么%20ThreadLocal%20采用弱引用,而不是强引用?在%20ThreadLocalMap%20上面有些注释,在这里摘录一部分,或许可以从中窥探一二:
To%20help%20deal%20with%20very%20large%20and%20long-lived%20usages,%20the%20hash%20table%20entries%20use%20WeakReferences%20for%20keys
翻译一下就是:为了解决非常大且长期使用的问题,哈希表使用了弱引用的%20key。假设,%20ThreadLocal%20使用的是强引用,会怎样呢?如果是强引用的话,在表格中也能够看出来,被强引用关联的对象,永远都不会被垃圾回收器回收掉如果引用的%20ThreadLocal%20对象被回收了,但是%20ThreadLocalMap%20还持有对%20ThreadLocal%20的强引用,如果没有%20remove%20的话,%20在%20GC%20时进行可达性分析,%20ThreadLocal%20依然可达,这样就不会对%20ThreadLocal%20进行回收,但是期望的是引用的%20ThreadLocal%20对象被回收,这样就不能达不到期望的目的了。使用弱引用的话,虽然会出现内存泄漏的问题,但是在%20ThreadLocal%20生命周期里面,都有对%20key%20值为%20null%20时进行回收的处理操作。所以,使用弱引用的话,可以在%20ThreadLocal%20生命周期中尽可能保证不出现内存泄漏的问题。在%20ThreadLcoal%20生命周期里面,都有对%20key%20值为%20null%20时进行回收的处理操作。
源码分析
ThreadLocal.set()
private%20void%20set(ThreadLocal<?>%20key,%20Object%20value)%20{%20%20%20%20//%20We%20don't%20use%20a%20fast%20path%20as%20with%20get()%20because%20it%20is%20at%20%20%20%20//%20least%20as%20common%20to%20use%20set()%20to%20create%20new%20entries%20as%20%20%20%20//%20it%20is%20to%20replace%20existing%20ones,%20in%20which%20case,%20a%20fast%20%20%20%20//%20path%20would%20fail%20more%20often%20than%20not.%20%20%20%20Entry[]%20tab%20=%20table;%20%20%20%20int%20len%20=%20tab.length;%20%20%20%20int%20i%20=%20key.threadLocalHashCode%20&%20(len-1);%20%20%20%20for%20(Entry%20e%20=%20tab[i];%20%20%20%20%20%20%20%20//%20如果%20e%20不为空,说明%20hash%20冲突,需要向后查找%20%20%20%20%20%20%20%20e%20!=%20null;%20%20%20%20%20%20%20%20//%20从这里可以看出,%20ThreadLocalMap%20采用的是开放地址法解决的%20hash%20冲突%20%20%20%20%20%20%20%20//%20是最经典的%20线性探测法%20-->%20我觉得之所以选择这种方法解决冲突时因为数据量不大%20%20%20%20%20%20%20%20e%20=%20tab[i%20=%20nextIndex(i,%20len)])%20{%20%20%20%20%20%20%20%20ThreadLocal<?>%20k%20=%20e.get();%20%20%20%20%20%20%20%20//%20要查找的%20ThreadLocal%20对象找到了,直接设置需要设置的值,然后%20return%20%20%20%20%20%20%20%20if%20(k%20==%20key)%20{%20%20%20%20%20%20%20%20%20%20%20%20e.value%20=%20value;%20%20%20%20%20%20%20%20%20%20%20%20return;%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20//%20如果%20k%20为%20null%20,说明有%20value%20没有及时回收,此时通过%20replaceStaleEntry%20进行处理%20%20%20%20%20%20%20%20//%20replaceStaleEntry%20具体内容等下分析%20%20%20%20%20%20%20%20if%20(k%20==%20null)%20{%20%20%20%20%20%20%20%20%20%20%20%20replaceStaleEntry(key,%20value,%20i);%20%20%20%20%20%20%20%20%20%20%20%20return;%20%20%20%20%20%20%20%20}%20%20%20%20}%20%20%20%20//%20如果%20tab[i]%20==%20null%20,则直接创建新的%20entry%20即可%20%20%20%20tab[i]%20=%20new%20Entry(key,%20value);%20%20%20%20int%20sz%20=%20++size;%20%20%20%20//%20在创建之后调用%20cleanSomeSlots%20方法检查是否有%20value%20值没有及时回收%20%20%20%20//%20如果%20sz%20>=%20threshold%20,则需要扩容,重新%20hash%20即,%20rehash();%20%20%20%20if%20(!cleanSomeSlots(i,%20sz)%20&&%20sz%20>=%20threshold)%20%20%20%20%20%20%20%20rehash();}
通过源码可以看到,在%20set%20方法中,主要是通过%20replaceStaleEntry%20方法和%20cleanSomeSlots%20方法去做的检测和处理
replaceStaleEntry
private%20void%20replaceStaleEntry(ThreadLocal<?>%20key,%20Object%20value,%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int%20staleSlot)%20{%20%20%20%20Entry[]%20tab%20=%20table;%20%20%20%20int%20len%20=%20tab.length;%20%20%20%20Entry%20e;%20%20%20%20//%20从当前%20staleSlot%20位置开始向前遍历%20%20%20%20int%20slotToExpunge%20=%20staleSlot;%20%20%20%20for%20(int%20i%20=%20prevIndex(staleSlot,%20len);%20%20%20%20%20%20%20%20(e%20=%20tab[i])%20!=%20null;%20%20%20%20%20%20%20%20i%20=%20prevIndex(i,%20len))%20%20%20%20%20%20%20%20if%20(e.get()%20==%20null)%20%20%20%20%20%20%20%20%20%20%20%20//%20当%20e.get()%20==%20null%20时,%20slotToExpunge%20记录下此时的%20i%20值%20%20%20%20%20%20%20%20%20%20%20%20//%20即%20slotToExpunge%20记录的是%20staleSlot%20左手边第一个空的%20Entry%20%20%20%20%20%20%20%20%20%20%20%20slotToExpunge%20=%20i;%20%20%20%20//%20接下来从当前%20staleSlot%20位置向后遍历%20%20%20%20//%20这两个遍历是为了清理在左边遇到的第一个空的%20entry%20到右边的第一个空的%20entry%20之间所有过期的对象%20%20%20%20//%20但是如果在向后遍历过程中,找到了需要设置值的%20key%20,就开始清理,不会再继续向下遍历%20%20%20%20for%20(int%20i%20=%20nextIndex(staleSlot,%20len);%20%20%20%20%20%20%20%20(e%20=%20tab[i])%20!=%20null;%20%20%20%20%20%20%20%20i%20=%20nextIndex(i,%20len))%20{%20%20%20%20%20%20%20%20ThreadLocal<?>%20k%20=%20e.get();%20%20%20%20%20%20%20%20//%20如果%20k%20==%20key%20说明在插入之前就已经有相同的%20key%20值存在,所以需要替换旧的值%20%20%20%20%20%20%20%20//%20同时和前面过期的对象进行交换位置%20%20%20%20%20%20%20%20if%20(k%20==%20key)%20{%20%20%20%20%20%20%20%20%20%20%20%20e.value%20=%20value;%20%20%20%20%20%20%20%20%20%20%20%20tab[i]%20=%20tab[staleSlot];%20%20%20%20%20%20%20%20%20%20%20%20tab[staleSlot]%20=%20e;%20%20%20%20%20%20%20%20%20%20%20%20//%20如果%20slotToExpunge%20==%20staleSlot%20说明向前遍历时没有找到过期的%20%20%20%20%20%20%20%20%20%20%20%20if%20(slotToExpunge%20==%20staleSlot)%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20slotToExpunge%20=%20i;%20%20%20%20%20%20%20%20%20%20%20%20//%20进行清理过期数据%20%20%20%20%20%20%20%20%20%20%20%20cleanSomeSlots(expungeStaleEntry(slotToExpunge),%20len);%20%20%20%20%20%20%20%20%20%20%20%20return;%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20//%20如果在向后遍历时,没有找到%20value%20被回收的%20Entry%20对象%20%20%20%20%20%20%20%20//%20且刚开始%20staleSlot%20的%20key%20为空,那么它本身就是需要设置%20value%20的%20Entry%20对象%20%20%20%20%20%20%20%20//%20此时不涉及到清理%20%20%20%20%20%20%20%20if%20(k%20==%20null%20&&%20slotToExpunge%20==%20staleSlot)%20%20%20%20%20%20%20%20%20%20%20%20slotToExpunge%20=%20i;%20%20%20%20}%20%20%20%20//%20如果%20key%20在数组中找不到,那就好说了,直接创建一个新的就可以了%20%20%20%20tab[staleSlot].value%20=%20null;%20%20%20%20tab[staleSlot]%20=%20new%20Entry(key,%20value);%20%20%20%20//%20如果%20slotToExpunge%20!=%20staleSlot%20说明存在过期的对象,就需要进行清理%20%20%20%20if%20(slotToExpunge%20!=%20staleSlot)%20%20%20%20%20%20%20%20cleanSomeSlots(expungeStaleEntry(slotToExpunge),%20len);}
在%20replaceStaleEntry%20方法中,需要注意一下刚开始的两个%20for%20循环中内容(在这里再贴一下):
if%20(e.get()%20==%20null)%20%20%20%20//%20当%20e.get()%20==%20null%20时,%20slotToExpunge%20记录下此时的%20i%20值%20%20%20%20//%20即%20slotToExpunge%20记录的是%20staleSlot%20左手边第一个空的%20Entry%20%20%20%20slotToExpunge%20=%20i;if%20(k%20==%20key)%20{%20%20%20%20e.value%20=%20value;%20%20%20%20tab[i]%20=%20tab[staleSlot];%20%20%20%20tab[staleSlot]%20=%20e;%20%20%20%20//%20如果%20slotToExpunge%20==%20staleSlot%20说明向前遍历时没有找到过期的%20%20%20%20if%20(slotToExpunge%20==%20staleSlot)%20%20%20%20%20%20%20%20slotToExpunge%20=%20i;%20%20%20%20%20%20%20%20//%20进行清理过期数据%20%20%20%20%20%20%20%20cleanSomeSlots(expungeStaleEntry(slotToExpunge),%20len);%20%20%20%20%20%20%20%20return;}
这两个%20for%20循环中的%20if%20到底是在做什么?看第一个%20if%20,当%20e.get()%20==%20null%20时,此时将%20i%20的值给%20slotToExpunge第二个%20if%20,当%20k%20==key%20时,此时将%20i%20给了%20staleSlot%20来进行交换为什么要对%20staleSlot%20进行交换呢?画图说明一下如下图,假设此时表长为%2010%20,其中下标为%203%20和%205%20的%20key%20已经被回收(%20key%20被回收掉的就是%20null%20),因为采用的开放地址法,所以%2015%20mod%2010%20应该是%205%20,但是因为位置被占,所以在%206%20的位置,同样%2025%20mod%2010%20也应该是%205%20,但是因为位置被占,下个位置也被占,所以就在第%207%20号的位置上了按照上面的分析,此时%20slotToExpunge%20值为%203%20,%20staleSlot%20值为%205%20,%20i%20为%206假设,假设这个时候如果不进行交换,而是直接回收的话,此时位置为%205%20的数据就被回收掉,然后接下来要插入一个%20key%20为%2015%20的数据,此时%2015%20mod%2010%20算出来是%205%20,正好这个时候位置为%205%20的被回收完毕,这个位置就被空出来了,那么此时就会这样:
同样的%20key%20值竟然出现了两次?!这肯定是不希望看到的结果,所以一定要进行数据交换在上面代码中有一行代码%20
cleanSomeSlots(expungeStaleEntry(slotToExpunge),%20len);%20,说明接下来的处理是交给了%20expungeStaleEntry%20,接下来去分析一下%20expungeStaleEntry
expungeStaleEntry
private%20int%20expungeStaleEntry(int%20staleSlot)%20{%20%20%20%20Entry[]%20tab%20=%20table;%20%20%20%20int%20len%20=%20tab.length;%20%20%20%20//%20expunge%20entry%20at%20staleSlot%20%20%20%20tab[staleSlot].value%20=%20null;%20%20%20%20tab[staleSlot]%20=%20null;%20%20%20%20size--;%20%20%20%20//%20Rehash%20until%20we%20encounter%20null%20%20%20%20Entry%20e;%20%20%20%20int%20i;%20%20%20%20for%20(i%20=%20nextIndex(staleSlot,%20len);%20%20%20%20%20%20%20%20(e%20=%20tab[i])%20!=%20null;%20%20%20%20%20%20%20%20i%20=%20nextIndex(i,%20len))%20{%20%20%20%20%20%20%20%20ThreadLocal<?>%20k%20=%20e.get();%20%20%20%20%20%20%20%20//%20如果%20k%20==%20null%20,说明%20value%20就应该被回收掉%20%20%20%20%20%20%20%20if%20(k%20==%20null)%20{%20%20%20%20%20%20%20%20%20%20%20%20//%20此时直接将%20e.value%20置为%20null%20%20%20%20%20%20%20%20%20%20%20%20%20//%20这样就将%20thread%20->%20threadLocalMap%20->%20value%20这条引用链给打破%20%20%20%20%20%20%20%20%20%20%20%20//%20方便了%20GC%20%20%20%20%20%20%20%20%20%20%20%20e.value%20=%20null;%20%20%20%20%20%20%20%20%20%20%20%20tab[i]%20=%20null;%20%20%20%20%20%20%20%20%20%20%20%20size--;%20%20%20%20%20%20%20%20}%20else%20{%20%20%20%20%20%20%20%20%20%20%20%20//%20这个时候要重新%20hash%20,因为采用的是开放地址法,所以可以理解为就是将后面的元素向前移动%20%20%20%20%20%20%20%20%20%20%20%20int%20h%20=%20k.threadLocalHashCode%20&%20(len%20-%201);%20%20%20%20%20%20%20%20%20%20%20%20if%20(h%20!=%20i)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20tab[i]%20=%20null;%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20Unlike%20Knuth%206.4%20Algorithm%20R,%20we%20must%20scan%20until%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20null%20because%20multiple%20entries%20could%20have%20been%20stale.%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20while%20(tab[h]%20!=%20null)%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20h%20=%20nextIndex(h,%20len);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20tab[h]%20=%20e;%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20}%20%20%20%20}%20%20%20%20return%20i;}
因为是在%20replaceStaleEntry%20方法中调用的此方法,传进来的值是%20staleSlot%20,继续上图,经过%20replaceStaleEntry%20之后,它的数据结构是这样:此时传进来的%20
staleSlot%20值为%206%20,因为此时的%20key%20为%20null%20,所以接下来会走%20e.value%20=%20null%20,这一步结束之后,就成了:接下来%20i%20为%207%20,此时的%20key%20不为%20null%20,那么就会重新%20hash%20:%20
int%20h%20=%20k.threadLocalHashCode%20&%20(len%20-%201);%20,得到的%20h%20应该是%205%20,但是实际上%20i%20为%207%20,说明出现了%20hash%20冲突,就会继续向下走,最终的结果是这样:可以看到,原来的 key 为 null ,值为 V5 的已经被回收掉了。所以回收掉之后,还要再次进行重新 hash ,就是为了防止 key 值重复插入情况的发生假设 key 为 25 的并没有进行向前移动,也就是它还在位置 7 ,位置 6 是空的,再插入一个 key 为 25 ,经过 hash 应该在位置 5 ,但是有数据了,那就向下走,到了位置 6 ,诶,竟然是空的,赶紧插进去,这不就又造成了上面说到的问题,同样的一个 key 竟然出现了两次?!而且经过
expungeStaleEntry 之后,将 key 为 null 的值,也设置为了 null ,这样就方便 GC分析到这里应该就比较明确了,在 expungeStaleEntry 中,有些地方是帮助 GC 的,而通过源码能够发现, set 方法调用了该方法进行了 GC 处理, get 方法也有,看下面的源码:
ThreadLocal.get()
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];// 如果能够找到寻找的值,直接 return 即可if (e != null && e.get() == key)return e;else// 如果找不到,则调用 getEntryAfterMiss 方法去处理return getEntryAfterMiss(key, i, e);}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)// 如果 k == null 说明有 value 没有及时回收// 调用 expungeStaleEntry 方法去处理,帮助 GCexpungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}
get 和 set 方法都有进行帮助 GC ,所以正常情况下是不会有内存溢出的,但是如果创建了之后一直没有调用 get 或者 set 方法,还是有可能会内存溢出。所以最保险的方法就是,使用完之后就及时 remove 一下,加快垃圾回收,就完美的避免了内存溢出。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论