面试官:为什么threadLocal有自动清除机制还存在内存泄漏么?

摘要:public void set(T value) { Thread t = Thread.currentThread; // 获取当前线程 ThreadLocalMap map = getmap(t); // 获取当前线程的threadLocalMap if

threadLocal中设置的值仅属于当前线程,该值对其他线程而言是隔离的,所以在同一时间并发修改一个属性的值也不会互相影响。

在使用ThreadLocal时,可以直接通过 set(T value) 、 get 来设置ThreadLocal的值、获取threadLocal的值。

public void set(T value) { Thread t = Thread.currentThread; // 获取当前线程 ThreadLocalMap map = getmap(t); // 获取当前线程的threadLocalMap if (map != null) { // 如果map不是空 map.set(this, value); // 设置值 } else { createMap(t, value); // 创建并设置值 }}// 获取线程的ThreadLocalMapThreadLocalMap getMap(Thread t) { return t.threadLocals; }// 对该ThreadLocal设置值private void set(ThreadLocal key, Object value) { // ThreadLocalMap内部的table数组 Entry tab = table; int len = tab.length; // 根据threadLocal的hash和长度进行与运算,找到下标位置 int i = key.threadLocalHashCode & (len-1); // 曾经该threadLocal有值,设置值并返回 for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) { // 获取entry的引用 ThreadLocal k = e.get; // 引用等于当前threadLocal 则进行设置值 if (k == key) { e.value = value; return; } // 当前引用为空,把key、value组装成entry放到i位置上,并清楚key为空的entry if (k == null) { replaceStaleEntry(key, value, i); return; } } // 组装entry tab[i] = new Entry(key, value); int sz = ++size; // 如果没有元素被清楚,并当前数组大小大于threshold则进行rehash; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash;}

其中 threshold = len * 2 / 3,它是通过setThreshold方法进行设置的。而每次rehash的时候都会调用resize方法,它会读取oldTable.length,把newLen设置为oldLen的两倍。这里有一个注意点int i = key.threadLocalHashCode & (len-1);下标是通过hash来确定的,会出现hash冲突,这里采用的是开放地址法来解决hash冲突,在下面的代码中有判断k==key,如果不相等则nextIndex(i, len)获取下一个下标来判断。

上述就是整个set的过程,下面来看一下get

public T get { Thread t = Thread.currentThread; // 获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // this为当前threadLocal,获取对应的entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { // 返回当前entry的值即可。 @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 设置初始值并返回,初始值是null return setInitialValue;}private Entry getEntry(ThreadLocal key) { // 查找下标 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get == key) // 找到对应entry进行返回 return e; else // 开始遍历entry数组,如果能找到key的entry就返回否则返回null return getEntryAfterMiss(key, i, e);}

get方法要比set简单很多,只是根据key找对应entry,把entry的值返回即可。

通过上述源码,可以总结出threadLocal的数据结构如下:

根据上面的介绍,可以看出一些潜在的问题;例如在使用threadLocal时堆栈信息如下:

当使用完threadLocal,threadLocal的对象引用就不存在了,而key对threadLocal是弱引用,gc后这段引用也不存在了。此时无法通过map.getEntry(this)找到对应的entry,而entry还一直存在Entry中,就有可能导致了内存溢出。 这里我写了是有可能导致内存溢出,例如在set方法中有这样一行代码

if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash;

该方法的具体代码如下:

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]; if (e != null && e.get == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed;}

当有新的threadlocal进行设置值时都会进行清除一下e.get == null引用为空的Entry,而进入到这里的条件是(n >>>= 1) != 0,当长度为16(10000)会触发5次,挨着当前threadlocal的Entry的连续5个都没有引用为null的话,就不会继续往下移除了。所以如果频繁的调用set方法,它也会帮助清除一些之前key已经被gc掉的entry对象,但无论如何如果没有gc和调用set方法的话,这些entry对象会一直在内存中占用。

所以每次在使用完threadlocal时要调用一下remove方法,它会自动把entry移除。

public void remove { ThreadLocalMap m = getMap(Thread.currentThread); if (m != null) { m.remove(this); }}

除此之外在threadlocal时,尽量把它设置为pricate static变量,这样因为threadLocal的强引用一直存在,不会被垃圾回收掉这样就能保证任何时间都可以找到Entry,并对其进行remove。

当ThreadLocal的引用在用户栈中已经移除了,并且没有调用remove方法;但是entry还有一个强引用指向threadLocal对象,e.get永远都不会是空,此时entry对象就永远无法被回收掉了。
这样弱引用比强引用就多一层保障弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocaI 调用 get/set/remove 中的任一方法的时候会被清除,从而避免内存泄漏。

public static void main(String args) { for (int i = 0 ; i

感兴趣的话,可以用上述示例跟着源码跑一遍源码的流程,当开启System.gc;时可以走到清理回收阶段。

不可以,如果想使用的话可以采用InheritableThreadLocal,它会在初始化子线程时进行设置子线程的threadlocal,也仅仅在初始化时有关联,后续子线程和父线程互相更改threadlocal都不会有任何影响。示例:

private static InheritableThreadLocal threadLocal = new InheritableThreadLocal;@SneakyThrowspublic static void main(String args) { threadLocal.set("1"); Thread thread = new Thread( -> { System.out.println("子线程获取threadLocal的值为:" + threadLocal.get); threadLocal.set("2"); } ); thread.start; Thread.sleep(200); System.out.println("父线程获取threadLocal的值为:" + threadLocal.get);}

1、父线程先设置threadLocal的值为1;
2、开启一个子线程,获取threadLocal的值,得到结果为1;
3、子线程设置threadLocal为2,并且get一下,得到的结果为2;
4、睡眠200ms确保子线程命令都执行完成;
5、父线程获取threadLocal的值,得到的结果为1。

来源:老男孩的成长之路一点号

相关推荐