相关文章:
今天组内同学总结处理 Sonar 扫描经验的时候提到了一点:“ThreadLocal
没有调用 remove()
方法,存在内存泄漏的风险” 。Sonar 完整描述如下:
Call “remove()” on “goodsImgMapThreadLocal”.
“ThreadLocal” variables should be cleaned up when no longer used
ThreadLocal
variables are supposed to be garbage collected once the holding thread is no longer alive. Memory leaks can occur when holding threads are re-used which is the case on application servers using pool of threads.To avoid such problems, it is recommended to always clean up
ThreadLocal
variables using theremove()
method to remove the current thread’s value for theThreadLocal
variable.In addition, calling
set(null)
to remove the value might keep the reference tothis
pointer in the map, which can cause memory leak in some scenarios. Usingremove
is safer to avoid this issue.Noncompliant Code Example
public class ThreadLocalUserSession implements UserSession { private static final ThreadLocal<UserSession> DELEGATE = new ThreadLocal<>(); public UserSession get() { UserSession session = DELEGATE.get(); if (session != null) { return session; } throw new UnauthorizedException("User is not authenticated"); } public void set(UserSession session) { DELEGATE.set(session); } public void incorrectCleanup() { DELEGATE.set(null); // Noncompliant } // some other methods without a call to DELEGATE.remove() }
Compliant Solution
public class ThreadLocalUserSession implements UserSession { private static final ThreadLocal<UserSession> DELEGATE = new ThreadLocal<>(); public UserSession get() { UserSession session = DELEGATE.get(); if (session != null) { return session; } throw new UnauthorizedException("User is not authenticated"); } public void set(UserSession session) { DELEGATE.set(session); } public void unload() { DELEGATE.remove(); // Compliant } // ... }
Exceptions
Rule will not detect non-private
ThreadLocal
variables, becauseremove()
can be called from another class.
其实整段说明最重要的是这一句:
In addition, calling
set(null)
to remove the value might keep the reference tothis
pointer in the map, which can cause memory leak in some scenarios. Usingremove
is safer to avoid this issue.
也就是说使用 set(null)
可以删除 value
,但是还是可能会存在 this
指针引用,使用 remove
可以避免这个问题。
ThreadLocal
中的数据存储是这样的:
Thread->threadLocals(ThreadLocalMap)->Entry[]->value
其实我们常说的 ThreadLocal
内存泄露,大部分都指的是 value
内存泄露:由于 Entry
对 ThreadLocal
是软引用,所以可能会出现 Threadlocal
被 GC 后,Entry
中的 value
还是存在,导致这个 value
无法被访问到,再加上现在基本都是使用的线程池,线程会复用,所以 threadLocals
一直存在一个强引用,最终会导致内存泄漏的风险,关于这个问题在《ThreadLocal 系列之 ThreadLocal 会内存泄漏吗?》中已经有过分析了,这里就不再赘述。
接下来先看看 ThreadLocal
的 remove
方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
本质是调用的 ThreadLocalMap
的 remove
方法:
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap
的 remove
方法的细节就没必要纠结了,因为从注释都能看出来了,直接就把当前这个 ThreadLocal
对应的 Entry
都删除了。Entry
都删除了,那 Entry
里面的 referent
和 value
自然就属于不可达对象,肯定可以被 GC。
接下来看下 ThreadLocal
的 set
方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
本质就是调用的 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);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
可以看到,如果 set
的 value
是 null
,要么就覆盖之前 Entry
中的 value
,要么就新建一个 Entry
,反正 Entry
是肯定存在的。这样就会有内存泄漏的风险,虽然这个风险非常低,毕竟一个 Entry
对象本身,即使从 Retained Heap 的角度看内存占用,也不会很大,但风险毕竟还是有的。
也发现大名鼎鼎的 Spring Cloud Sleuth 也出现过这样的问题,也有人提过 Issue(https://github.com/spring-cloud/spring-cloud-sleuth/issues/27)。在后来的版本中也有修复(https://github.com/spring-cloud/spring-cloud-sleuth/commit/44e4a2d26b5e9ec63ec497f5b651b74b9bebb8ca),也是将 set(null)
修改为 remove
:
最后做个总结,其实 set(null)
和 remove
的区别就在于前者仅仅是将 value
设置为 null
,但是整个键值还是存在的,而后者是直接将整个键值都删除,所以很明显使用 remove
更合适。
欢迎关注公众号: