关于ThreadLocal的用法、特性、原理……内容不进行过多描述,本文主要用于自己理解ThreadLocal结构为什么要remove用,需要一定的ThreadLocal基础。文章不打算进行过多书面描述,更多的是口语化内容。

工作原理

简单的说,Thread中有一个变量是ThreadLocalMap跟随着线程的销毁而销毁,相当于是每个线程都有自己的一份ThreadLocalMap,它可以按照字面意思理解,就是一个封装的Map,使用ThreadLocal.set(xx)/ThreadLocal.get()实际上是往这个Map中存值或者取值。那么ThreadLocal去哪里了?——ThreadLocal是这个Map中的key,并且这个key是软引用,初学者时候的我在使用ThreadLocal时以为这个东西的key是Thread。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

不remove会内存泄漏?

刷到过网上很多说法是ThreadLocal在使用完后不进行remove的话会出现内存泄漏,因为什么key是软引用的巴拉巴拉一堆,但是事实真的是这样的吗?

ThreadLocal作为局部变量

首先这个并不属于我们的常规用法,我们更多的是使用ThreadLocal作为全局变量。但这个例子能够更好的去理解ThreadLocal什么情况下会存在内存泄漏情况。

假设我们的spring boot程序使用tomcat的方式运行着,我们tomcat web容器的线程会进行复用,假设核心线程Thread1一直在工作,而工作的任务中的代码逻辑是这样的

  1. new一个ThreadLocal,ThreadLocal<Object> tl = new ThreadLocal<>();
  2. 往ThreadLocal里面set一些值
  3. 调用其他方法做处理,其他方法中使用ThreadLocal获取值
  4. 方法执行完成

这个过程中缺少了remove,那么会出现什么情况呢。

  1. 线程Thread1一直执行这个相同的任务,代码中每一次执行都会创建一个ThreadLocal,并往这个ThreadLocal中set值。
  2. 前面说到ThreadLocal实际是作为了ThreadLocalMap的key,也就是说Thread1中含有的ThreadLocalMap在被不停的写入(ThreadLocal@1,value1),(ThreadLocal@2,value2),(ThreadLocal@3,value3)。
  3. 当方法依次执行完成后,ThreadLocal@1/2/3……因为是局部变量,所以都失去了本身tl的强引用,只剩下ThreadLocalMap对key的软引用,由于是软引用,所以ThreadLocal@1/2/3……是能够被正常gc回收掉的。
  4. 那么value呢?此时的下面引用链是这样的
    Thread->ThreadLocalMap->ThreadLocal(软引用)/Value(强引用)
    这个线程是核心线程一直存活,导致了很多个value的强引用也一直存在无法被回收,最终导致内存泄漏引发OOM,能清理这个value的只有方法执行中的局部变量ThreadLocal.remove。

ThreadLocal作为全局变量

ThreadLocal作为全局变量逻辑就较为简单了,同一个线程Thread1中的ThreadLocalMap一直是同一份,而全局变量ThreadLocal也一直都是同一份,所以即使Thread1不停的执行set方法,最终由于ThreadLocalMap中的key(也就是全局变量ThreadLocal)都相同,所以会把上一次set的值进行覆盖,并不会存在所谓的内存泄漏。当然,当ThreadLocal作为全局变量使用的时候,似乎它涉及的软引用就没有产生作用了,因为这个全局变量一直会保持着一个强引用。

为什么还要remove

既然常规用法中的ThreadLocal作为全局变量的时候不会发生内存泄漏,我们为什么还要去remove?
因为不remove会导致数据混淆或受到外部攻击

  1. 还是假定在web容器中,Thread1依旧是核心线程。
  2. 代码中有一个全局变量TokenThreadLocal,用于存放请求的用户token,token用于鉴权。
  3. 假定用户1访问系统是Thread1处理的请求,而请求执行完毕后TokenThreadLocal没有进行remove。
  4. 这时有外部攻击访问了系统的某个接口,外部攻击在没有携带token的情况下理应无法访问系统,但当外部攻击请求是由Thread1执行的时候,这个请求内执行到类似TokenThreadLocal.get()的代码,此时将会获取到用户1的token,发生预料之外的事故。
  5. 当然,这个事故中第一责任应该是系统鉴权的设计,系统鉴权不会也不应该那么简单,这个例子也只是方便记忆关于需要remove的问题。

暂定完结

后面又想到了什么再进行更新吧。

另一方面,当ThreadLocal作为全局变量使用时,由于ThreadLocalMap持有的ThreadLocal是全局唯一的,同一个线程中重复执行相同的代码将只会在ThreadLocalMap中存储一份键值对。但即使如此,也需要在不再需要时调用remove()方法来清理存储的数据。例如,在Web程序中,如果使用ThreadLocal来存储用户token,在不同请求之间线程复用可能会导致资源混淆,例如前一次请求的用户token可能被后续的请求错误地复用。

1. 及时清理: 对于存储在ThreadLocal中的数据,一旦完成使用,应该立即调用remove()方法来清除。这是防止内存泄漏的最简单有效的方法。

2. 避免使用局部变量ThreadLocal: 在可能的情况下,避免将ThreadLocal作为局部变量使用。这样可以减少ThreadLocal实例的创建和销毁,避免内存泄漏的风险。

3. 监控和诊断: 在复杂的应用中,定期监控和诊断可以帮助发现潜在的ThreadLocal相关的内存泄漏问题。使用Java性能监控工具(如JProfiler或VisualVM)可以帮助识别和解决这类问题。

4. 谨慎使用全局ThreadLocal: 尽管将ThreadLocal作为全局变量使用可以减少内存泄漏的风险,但仍需注意确保在适当的时机清除数据,特别是在用户会话结束或请求完成时。

## 结论

ThreadLocal是一个强大的工具,它为线程提供了私有的数据存储。然而,不正确的使用可能会引起内存泄漏等问题。通过遵循上述最佳实践和解决方案,开发者可以有效地利用ThreadLocal,同时避免潜在的问题。