相关文章:
谈到 ThreadLocal
在使用的时候的注意事项,其中有一个就是内存泄露,那么 ThreadLocal
会发生内存泄漏吗?或者说什么情况下 ThreadLocal
会内存泄漏呢?
存入超大对象
无须多言,存入“超大对象”肯定会有内存泄漏的风险。
key 为 null 造成内存泄漏
这里引用一篇博客的观点:
Threadlocal 里面使用了一个存在弱引用的 Map,当释放掉 Threadlocal 的强引用以后,Map里面的 value 却没有被回收,而这块 value 永远不会被访问到了, 所以存在着内存泄露。
https://my.oschina.net/u/658658/blog/1830812
很多文章都是这个观点,主要说法是:由于 ThreadLocal.ThreadLocalMap.Entry
中的 key
是弱引用,会存在 key
为 null
的情况,这时候 value
无法被访问到,但是由于 value
被 Entry
关联,最主要原因是大部分情况下会使用线程池,造成 value
所占内存无法被释放,从而导致内存泄漏。
其实我们一般使用都会将 ThreadLocal
作为一个类级别变量,会被强引用,这样使用是不会出现"key
为 null
的情况"。
但是不可否认的是,如果将 ThreadLocal
作为线程池中线程的局部变量的确会出现"key
为 null
的情况"。可以先看一个例子:
public class ThreadLocalOomDemo1 {
private static final ExecutorService FIXED_EXECUTOR = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
FIXED_EXECUTOR.submit(() -> {
ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected void finalize() {
System.out.println("finalize........");
}
};
threadLocal.set("a");
System.out.println(threadLocal.get());
});
System.out.println("-------------------");
sleep(100);
System.gc();
FIXED_EXECUTOR.submit(()->{
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
});
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
[0.002s][warning][gc] -XX:+PrintGC is deprecated. Will use -Xlog:gc instead.
[0.013s][info ][gc] Using G1
-------------------
a
[0.296s][info ][gc] GC(0) Pause Full (System.gc()) 5M->3M(17M) 4.705ms
finalize........
pool-1-thread-1
Debug
调试:
发现的确会存在出现 key
为 null
,已被 GC 的情况,此时 value
是无法获取的。
ThreadLocalMap#set
ThreadLocal
中的数据本质是存储到 ThreadLocalMap
中的,看下 set 方法:
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);
//出现 hash 冲突,使用开放地址法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 相同,覆盖
if (k == key) {
e.value = value;
return;
}
//key 为 null
if (k == null) {
//进入替换方法
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
当出现 hash
冲突的时候,使用开放地址法向后查找,出现 key
为 null
,通过replaceStaleEntry
方法处理。
ThreadLocalMap#get
同样地,再看看 get
方法:
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
//发生 hash 冲突
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)
//删除过期数据
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
也就是说当出现 hash
冲突的时候,get
方法继续查找,同样会清除循环中遇到的过期 entry
。
总结
ThreadLocal
的确会出现内存溢出的情况,但是其实说ThreadLocal
是否会出现内存泄漏,这个表述本身就有问题,真正应该关注的是在线程池中复用的Thread
对象的成员变量是否会出现内存泄漏,- 不能直接以
key
出现null
,value
无法被访问回收作为缘由,我们平常都是将ThreadLocal
作为类级别变量,几乎不可能会出现key
为null
的情况,即使出现了这样的情况,ThreadLocal
在设计时 通过get
和set
方法已经做了key
为null
的优化处理; - 使用完
ThreadLocal
后,都应该调用它的remove
方法,避免可能出现的一段时间数据保留(如存入一个大对象,key
GC 后,未出现hash
冲突,那么这个对象会有一段时间长期存在),也可以避免线程复用时出现脏数据;
References
- https://www.jianshu.com/p/56f64e3c1b6c
- https://www.iteye.com/blog/liuinsect-1827012
欢迎关注公众号