0
点赞
收藏
分享

微信扫一扫

小议 ThreadLocal 中的 remove() 和 set(null)


相关文章:

  • ​​ThreadLocal 系列之 ThreadLocal 会内存泄漏吗?​​

今天组内同学总结处理 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 the ​​remove()​​​ method to remove the current thread’s value for the ​​ThreadLocal​​ variable.

In addition, calling ​​set(null)​​​ to remove the value might keep the reference to ​​this​​​ pointer in the map, which can cause memory leak in some scenarios. Using ​​remove​​ 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, because ​​remove()​​ can be called from another class.

其实整段说明最重要的是这一句:

In addition, calling ​​set(null)​​​ to remove the value might keep the reference to ​​this​​​ pointer in the map, which can cause memory leak in some scenarios. Using ​​remove​​ 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​​:

小议 ThreadLocal 中的 remove() 和 set(null)_spring

最后做个总结,其实 ​​set(null)​​​ 和 ​​remove​​​ 的区别就在于前者仅仅是将 ​​value​​​ 设置为 ​​null​​​ ,但是整个键值还是存在的,而后者是直接将整个键值都删除,所以很明显使用 ​​remove​​ 更合适。


小议 ThreadLocal 中的 remove() 和 set(null)_内存泄漏_02


举报

相关推荐

0 条评论