什么是thread local?
线程局部(thread local)是在学习并发时,会经常遇到的一个专业名词,它是一种通用的概念,而非java专有的抽象。即表示线程私有的变量,从而直接避免了使用共享数据所需要面临的同步问题。
ThreadLocal类
ThreadLocal类是java对线程局部提供的java api。它的对外提供的方法有点类似java 8中的Optional,通过一个容器来封装具体的值。
public T get() {
//...
}
public void set(T value) {
//...
}
而在内部实现中,它是通过一个ThreadLocalMap实现,每个Thread对象都有一个唯一的ThreadLocalMap类型的变量。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap类
ThreadLocalMap类似WeakHashMap,特点是采用比较特殊的HashMap实现:
- 采用弱键,通过一个继承WeakReference的Entry来引用键。
- 使用线性探测法来解决哈希冲突问题。
在ThreadLocal中,使用ThreadLocal本身作为键,插入数据到ThreadLocalMap中。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
被继承WeakReference的Entry所引用:
static class Entry extends WeakReference<ThreadLocal<?>> {
}
ThreadLocalMap与内存泄露
当ThreadLocal实例本身没有强引用,导致Entry中的弱键被GC回收时,value就成了无效数据,但因为是强键而存在于内存中,这就产生了内存泄露。
(图片来自http://www.cnblogs.com/onlywujun/p/3524675.html)
为了最小化内存泄露的可能性,ThreadLoal在调用get(),set(),remove()方法时,将会触发ThreadLocalMap中对无效value的清理方法。但如果没有调用相关方法,内存泄露就会一直存在。[^1]
因此最佳实践是,使用完ThreadLocal后,调用remove()方法对value进行清理。
ThreadLocal会被回收吗?
ThreadLocalMap使用WeakReference来关联ThreadLocal对象,如果ThreadLcoal对象没有了强引用,那么ThreadLocalMap中的WeakReference将可能被清理,导致value无效。
那么在Web开发中,可能出现一个请求的处理过程中,保存在ThreadLocal中的User等数据因为GC丢失吗?
答案是不会。
一般情况下,ThreadLocal实例是作为类的静态变量或静态常量来使用的,如:
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
该实例被类的Class对象所强引用,而类的回收条件是很苛刻的[^2]:
- 类的所有实例被回收
- 该类的ClassLoader被回收
- 该类的Class对象没有被任何地方引用
因为一般情况下,我们不会使用特殊的ClassLoader来加载ThreadLocal实例所在类,因此不用担心ThreadLocal被回收。
如果你对特殊情况感兴趣,也可以参考stackoverflow上面的示例[^3]写一个使用自定义ClassLoader而被回收的类属性的ThreadLocal,如下:
自定义ClassLoader:
public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL... urls) {
super(urls, null);
}
@Override
protected void finalize() {
System.out.println("*** CustomClassLoader finalized!");
}
}
测试类:
public class ThreadLocalGCTest {
public static void main(String[] args)throws Exception{
while (true) {
loadFoo();
System.gc();////此时Foo.class和CumstomClassLoader.class将被回收
Thread.sleep(1000);
}
}
private static void loadFoo() throws Exception {
CustomClassLoader cl = new CustomClassLoader(new URL("file:C:\\Users\\anonym\\Projects\\java-learning\\out\\production\\java-learning\\"));//根据自己的情况改变路径
Class<?> clazz = cl.loadClass("thread.threadlocal.ThreadLocalGCTest$Foo");
clazz.newInstance();
cl = null;
}
public static class Foo {
private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>();
public Foo() {
tl.set(50);
System.out.println("ClassLoader: " + this.getClass().getClassLoader());
}
@Override
protected void finalize() {
System.out.println("*** Foo finalized!");
}
}
}
测试结果:
ClassLoader: thread.threadlocal.CustomClassLoader@372f7a8d
*** Foo finalized!
*** CustomClassLoader finalized!
析构函数执行,说明类已经被卸载。此时的ThreadLocal实例也已经被回收。
[^1]http://www.cnblogs.com/onlywujun/p/3524675.html
[^2]《深入理解Java虚拟机 第二版》
[^3] https://stackoverflow.com/questions/17968803/threadlocal-memory-leak