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

相关文章:

今天组内同学总结处理 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 内存泄露:由于 EntryThreadLocal 是软引用,所以可能会出现 Threadlocal 被 GC 后,Entry 中的 value 还是存在,导致这个 value 无法被访问到,再加上现在基本都是使用的线程池,线程会复用,所以 threadLocals 一直存在一个强引用,最终会导致内存泄漏的风险,关于这个问题在《ThreadLocal 系列之 ThreadLocal 会内存泄漏吗?》中已经有过分析了,这里就不再赘述。

接下来先看看 ThreadLocalremove 方法:

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

本质是调用的 ThreadLocalMapremove 方法:

/**
 * 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;
        }
    }
}

ThreadLocalMapremove 方法的细节就没必要纠结了,因为从注释都能看出来了,直接就把当前这个 ThreadLocal 对应的 Entry 都删除了。Entry 都删除了,那 Entry 里面的 referentvalue 自然就属于不可达对象,肯定可以被 GC。

接下来看下 ThreadLocalset 方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

本质就是调用的 ThreadLocalMapset 方法:

        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();
        }

可以看到,如果 setvaluenull ,要么就覆盖之前 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 更合适。


欢迎关注公众号:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值