关于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一直在工作,而工作的任务中的代码逻辑是这样的
- new一个ThreadLocal,
ThreadLocal<Object> tl = new ThreadLocal<>();
- 往ThreadLocal里面set一些值
- 调用其他方法做处理,其他方法中使用ThreadLocal获取值
- 方法执行完成
这个过程中缺少了remove,那么会出现什么情况呢。
- 线程Thread1一直执行这个相同的任务,代码中每一次执行都会创建一个ThreadLocal,并往这个ThreadLocal中set值。
- 前面说到ThreadLocal实际是作为了ThreadLocalMap的key,也就是说Thread1中含有的ThreadLocalMap在被不停的写入(ThreadLocal@1,value1),(ThreadLocal@2,value2),(ThreadLocal@3,value3)。
- 当方法依次执行完成后,ThreadLocal@1/2/3……因为是局部变量,所以都失去了本身tl的强引用,只剩下ThreadLocalMap对key的软引用,由于是软引用,所以ThreadLocal@1/2/3……是能够被正常gc回收掉的。
- 那么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会导致数据混淆或受到外部攻击
- 还是假定在web容器中,Thread1依旧是核心线程。
- 代码中有一个全局变量TokenThreadLocal,用于存放请求的用户token,token用于鉴权。
- 假定用户1访问系统是Thread1处理的请求,而请求执行完毕后TokenThreadLocal没有进行remove。
- 这时有外部攻击访问了系统的某个接口,外部攻击在没有携带token的情况下理应无法访问系统,但当外部攻击请求是由Thread1执行的时候,这个请求内执行到类似TokenThreadLocal.get()的代码,此时将会获取到用户1的token,发生预料之外的事故。
- 当然,这个事故中第一责任应该是系统鉴权的设计,系统鉴权不会也不应该那么简单,这个例子也只是方便记忆关于需要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
,同时避免潜在的问题。