> 文章列表 > ThreadLocal原理分析及内存泄漏

ThreadLocal原理分析及内存泄漏

ThreadLocal原理分析及内存泄漏

ThreadLocal原理分析及内存泄漏

    • ThreadLocal的使用
    • ThreadLocal原理
      • set方法解析
      • replaceStaleEntry方法解析
      • expungeStaleEntry方法解析
      • cleanSomeSlots方法解析
        • case 1: 向前有脏数据,向后找到可覆盖的Entry
        • case 2: 向前有脏数据,向后未找到可覆盖的Entry
        • case 3: 向前没有脏数据,向后找到可覆盖的Entry
        • case 4: 向前没有脏数据,向后未找到可覆盖的Entry
      • get方法解析
    • ThreadLocal内存泄漏
    • Why key is ThreadLocal?

ThreadLocal意为线程本地变量,它会在每个线程创建一个数据副本,所以可以用来解决多线程并发时访问共享变量的问题。

ThreadLocal的使用

  • set()
    在当前线程范围内,设置一个值存储到ThreadLocal中,这个值仅对当前线程可见。
    相当于在当前线程范围内建立了副本。
  • get()
    从当前线程范围内取出set方法设置的值.
  • remove()
    移除当前线程中存储的值

ThreadLocalMap里的Entry使用的key是对ThreadLocal对象的弱引用, 当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些key
ThreadLocal原理分析及内存泄漏

ThreadLocal原理

ThreadLocal 能够实现线程间的隔离,所以当前线程保存的数据,只会存储在当前线程范围内。-> 数据是线程私有的 --> 每个线程有自己的ThreadLocalMap -> key 为ThreadLocal对象

set方法解析

   public void set(T value) {Thread t = Thread.currentThread();// 如果当前线程已经初始化了map,则获取这个map,没有则进行初始化ThreadLocalMap map = getMap(t);if (map != null) //修改valuemap.set(this, value);else //初始化并设置value值createMap(t, value);}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY]; //默认长度为16的数组int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //计算数组下标table[i] = new Entry(firstKey, firstValue); //把key/value存储到下标为i的位置.size = 1;setThreshold(INITIAL_CAPACITY);}//注意下这里的Entry对ThreadLocal是一个弱引用static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
 private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;int len = tab.length;//计算数组下标int i = key.threadLocalHashCode & (len-1);//线性探索,从i开始向后探索,如果遇到相同的key可以替换value后退出;如果遇到key==null就需要清理旧entry,在探索过程中,如果有个entry==null那么就说明这里有个空位,直接存放数据就行了(结束循环)for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//如果在i的位置存在key且和当前要设置的key是一个,则直接替换。if (k == key) {e.value = value;return;}//如果key==null,则认为这个位置可能之前存放过旧的entry,不能直接赋值replaceStaleEntry(清理过期数据并将当前value保存到entry数组中) 放到后面单独分析下if (k == null) {replaceStaleEntry(key, value, i);return;}}//执行到这里就说明tab[i]上没有数据,可以存放entry(注意前面的循环结束条件)  一定会有空位的,否则在上次新增entry的时候会触发扩容tab[i] = new Entry(key, value);int sz = ++size;//cleanSomeSlots返回真说明有stale entry被清空了,size肯定减小了;//只有当 cleanSomeSlots返回假 且到达阈值时,才肯定需要rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}

ThreadLocal中的线性探索
1.向前/向后查找stale的节点
2.向后查找可覆盖的节点 (可以结合后面的图来理解)
3.清理节点并调整部分节点的位置

replaceStaleEntry方法解析

    //staleSlot是当前key==null的下标(就是一个可能已过期的下标位置,对应的key可能被GC回收了) 可以结合下面的图来理解这个private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;ThreadLocal.ThreadLocalMap.Entry e;//记录下可能需要被清除的index(从stableSlot开始),向前探索直到找到entry==null的位置int slotToExpunge = staleSlot;for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;//向后探索,去查找key==目标key的位置,遇到tab[i]==null则结束循环for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//找到后将staleSlot和当前位置i的元素互换if (k == key) {e.value = value;tab[i] = tab[staleSlot];tab[staleSlot] = e;// 如果相同,则说明staleSlot之前没有待清理的entry,这个条件只会成立一次if (slotToExpunge == staleSlot)//(1)这里的slotToExpunge其实等于nextIndex(staleSlot, len)slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// If we didn't find stale entry on backward scan, the// first stale entry seen while scanning for key is the// first still present in the run.//如果staleSlot之前没有待清理的entry(slotToExpunge == staleSlot)且k==null,说明位置i的entry需要清理,此时的slotToExpunge为staleSlot最近的一个需要清理的entry下标if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slot//如果没有找到可覆盖的key,直接将staleSlot的位置清理到,存放现在的值就行tab[staleSlot].value = null;tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);// 如果探索过程中有发现其他stale节点,则清理它们if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

expungeStaleEntry方法解析

   private int expungeStaleEntry(int staleSlot) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;// staleSlot节点的数据是一定要清理的,因为其key==nulltab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter nullThreadLocal.ThreadLocalMap.Entry e;int i;//从staleSlot向后遍历开始清理节点(遇到entry==null的情形,退出清理)for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//清理k==null的节点if (k == null) {e.value = null;tab[i] = null;size--;} else {//k!=null,由于清理了前面的数据,那么就重新计算下标,将节点tab[i]存放到更接近正确下标的位置,方便下次查询int h = k.threadLocalHashCode & (len - 1);if (h != i) {//移动tab[i]的entry到更前面的位置,所以这里可以置为nulltab[i] = null;while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}//i为结束循环的下标,tab[i]==null(i是staleSlot之后第一个为entry==null的位置)return i;}

cleanSomeSlots方法解析

    //从entry==null的位置开始清理(前面的数组在expungeStaleEntry()里清理了)private boolean cleanSomeSlots(int i, int n) {//设置清空标志,是否清除过数据boolean removed = false;Entry[] tab = table;int len = tab.length;//循环清除,最少循环log2(n)次do {i = nextIndex(i, len);Entry e = tab[i];//找到key为null的节点,则进行清除if (e != null && e.get() == null) {//重新赋值最新的len给n,如果有找到可以清除的数据,那么n就会恢复到len,又重新开始清理n = len;removed = true;//从i开始查找下一个tab[i]==null的下标i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0); //无符号右移,若没有找到任何可以清理的数据,则循环执行log2(n)次//返回清除标志return removed;}

总结下这里的清理逻辑:

向前找key == null的第一个脏数据(没有则向后找第一个脏数据,记为slotToExpunge),向后找可覆盖的entry(有则替换), 如果找到脏数据,那么就从slotToExpunge 开始清理(最终目标是将从slotToExpunge 到len的数据都清理一遍)

清理的时候会将key==null的entry的value也置为空,同时会将清理过程中遇到的key!=null的节点调整位置

case 1: 向前有脏数据,向后找到可覆盖的Entry

ThreadLocal原理分析及内存泄漏
ThreadLocal原理分析及内存泄漏

case 2: 向前有脏数据,向后未找到可覆盖的Entry

ThreadLocal原理分析及内存泄漏

case 3: 向前没有脏数据,向后找到可覆盖的Entry

ThreadLocal原理分析及内存泄漏

case 4: 向前没有脏数据,向后未找到可覆盖的Entry

ThreadLocal原理分析及内存泄漏
为什么在进行线性探索的时候遇到key==null的entry不可以直接存储到该位置?

前面有提到ThreadLocalMap里的Entry使用的key是对ThreadLocal对象的弱引用, 当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些key,此时,ThreadLocalMap中就会出现一些key为null,但是value不为null(也可能用户设置的value就是null)的Entry,这些Entry如果用户不主动清理,就会一直保留在ThreadLocalMap中。

ThreadLocal底层数据是用一个entry数组来存储的,key为ThreadLocal对象,value为用户设置的值
在使用hashCode来计算索引的时候肯定会有hash冲突的问题,如果有hash冲突(假设为位置i),那么新来的key只能存放在i的下一个为空的位置。当你发现table[i]上的keynull,它可能是没有存储过任何数据,也可能是原来的key被GC回收了,所以就需要进行线性探索,判断后续的数组位置上是否有key目前对象的数据,有的话需要替换下标。

有人会问,为什么找到相同key的位置后需要替换呢,因为在set()的时候会判断tab[i]==null,如果我们把数据存放在正确位置i的后面,等下一次再set的时候,如果tab[i]==null,那么就会直接保存在i的位置上,导致一个Map里存在相同key

get方法解析

    public T get() {Thread t = Thread.currentThread();//获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//查找对应的valueThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果没有存储过,则返回一个初始化值return setInitialValue();}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//如果没有在正确的index下找到该数据,可能是发生hash冲突导致位置发生了改变return getEntryAfterMiss(key, i, e);}
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;//从i开始向后遍历,因为set的时候也是set到第一个不为null的值,所以只需要遍历到第一个为null的位置即可while (e != null) {ThreadLocal<?> k = e.get();//key相等,直接返回valueif (k == key)return e;//key为null,说明该Entry是 stale节点,需要清除if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}

ThreadLocal内存泄漏

当ThreadLocal被回收后,ThreadLocalMap中对应的key就会指向null,而对应value却不为null,这些value项如果不主动清理,就会一直驻留在ThreadLocalMap中。

从前面的源码分析,可以知道 expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现
get 和set 方法都可能触发清理方法 expungeStaleEntry() ,所以正常情况下是不会有内存溢出的 但
是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,所以在使用ThreadLocal的时候可以在不再需要该对象的时候手动调用remove()方法,加快垃圾回收,避免内存溢出

一般情形下,线程结束的时候,也就没有强引用再指向ThreadLocal 中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会和线程一起被回收掉,但是当你使用的线程是线程池时, 由于这个线程在执行完的时候并不会销毁,而是归还给线程池,就会出现内泄漏的问题。

Why key is ThreadLocal?

在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,但是现在已经不是这样了。

JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的要隔离的变量(这里的key不能是Thread,因为一个线程可能会定义多个ThreadLocal), Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;

JDK8之后设计的好处在于:

  1. 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。
  2. 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁