ThreadLocal是Java所提供的"线程封闭的"一种机制,其会为每个线程都创建一份变量的副本,以保障线程安全。
其目的与synchronized完全不一致。synchronized保证了多线程环境下数据的一致性,而ThreadLocal却是保证了多线程环境下数据的独立性。
有两种方式可以构造一个ThreadLocal对象。
第一种是直接通过new的方式,如下:
ThreadLocal<String> t = new ThreadLocal();
或者可以通过静态方法ThreadLocal#withInitial(Suppiler)来构造:
ThreadLocal<String> t = ThreadLocal.withInitial(() -> "initial value");
这种方式实际上构造的是一个SuppliedThreadLocal对象。该类是ThreadLocal的一个子类。其内部保存静态方法所传入的suppiler函数用于**在从未设置****value**的情况下能够让调用者获取到一个初始化的值。
在ThreadLocal构建完毕之后,就可以通过set()和get()和remove()来操作对应的值了。
t.set("another value"); // 设置ThreadLocal的值
String value = t.get(); // 获取ThreadLocal的值
t.remove(); // 删除ThreadLocal的值
ThreadLocal内部原理
在每个Thread对象中,都有一个类型为ThreadLocal.ThreadLocalMap的对象,名为threadLocals。这是真正存储ThreadLocal值的地方,并且,该字段由ThreadLocal维护,而非Thread对象本身管理。
ThreadLocalMap内部维护了一个Entry类型的数据,名为table。Entry是ThreadLocalMap的静态内部类,其类定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry是一个K:V结构的类,其中Key为ThreadLocal对象,且是一个弱引用(WeakReference)对象,Value是使用者所设置的值。
根据以上信息可知,ThreadLocal整体结构如下:
内存泄漏风险
在ThreadLocal的底层存储结构ThreadLocalMap.Entry中,ThreadLocal 对象作为Key且以弱引用的形式存在。
弱引用具备如下特性:
通过
WeakReference类来使用,在没有其他强引用关系的情况下,弱引用对象会在下一次垃圾收集时回收掉
通过上图可知,一般情况下,只有Defined Class(指开发者所定义的,实例化ThreadLocal的类)的对应对象与ThreadLocal对象存在直接的强引用关系。也就是说当Defined Class实例被GC回收之后,就很容易造成ThreadLocalMap.Entry中Key的内存空间被回收,从而变为null值。
现代应用大量使用池化技术来复用线程,线程对象不一定能够被回收。在这样的情况下,Value存在"Thread —> TheadLocalMap —> ThreadLocalMap.Entry —> Value"的强引用关系链,也无法被回收。但在其Key 被回收的情况下,开发者永远也无法访问到这个Value值,从而导致了内存泄漏。
内存泄漏解决方案
ThreadLocalMap中存在一个名为cleanSomeSlots的方法,其会扫描Key为null的Entry,并将其删除掉。
这个方法在调用ThreadLocal的get() 、set(Object) 和remove()方法时都会被调用到。因此,在大部分的场景下,ThreadLocal出现内存泄漏的几率已经比较小了。
不过我们也可以通过两个方法来"加强防御"。
第一种方法考虑将ThreadLocal对象设置为静态对象。
public class Main {
public static ThreadLocal<String> t = new ThreadLocal();
}
此时,ThreadLocal和Class对象产生了强引用关系,除非发生"类型卸载",ThreadLocal对象都不会被回收掉。
这种方式会使用ThreadLocalMap.Entry的Key和Value都不会被回收掉,且能够被指定的线程所访问到。但缺点也很明显。若ThreadLocal所存储的数据本就只是临时使用,那"永不回收"的Key 和Value实际上就成了另一种"内存泄漏"。
更好的方式是手动回收,即使用之后手动调用ThreadLocal#remove()方法。
ThreadLocal<String> t = new ThreadLocal<>()
try {
t.set(obj);
doSomething();
} finally {
t.remove();
}
这种方式虽然不够灵活,但却是目前使用ThreadLocal最为推荐的方式。