通常情况下,我们在内存中创建的变量是可被多个线程同时访问的,Java通过ThreadLocal实现了线程数据隔离的机制。
ThreadLocal的用法
既然前面提到了ThreadLocal存储的变量是线程隔离的,我们不妨就测试一下是否如我们所说。我们先创建两个线程,然后为这两个线程设置ThreadLocal变量。
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
threadLocal.set("this is t1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
}, "t1");
Thread t2 = new Thread(() -> {
threadLocal.set("this is t2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
}, "t2");
t1.start();
t2.start();
}
}
上面的代码输出下面两行结果,说明ThreadLocal的变量确实是线程间隔离的。
t1: this is t1
t2: this is t2
ThreadLocal的原理
开始讲ThreadLocal原理之前,我们需要简单了解一下ThreadLocalMap。在Thread类中有个threadLocals
变量,这是一个ThreadLocal.ThreadLocalMap
类型的变量,也就是说ThreadLocalMap是ThreadLocal的静态内部类。从名字可以看出ThreadLocalMap同样是一个Map,Map的key是ThreadLocal对象,这里的ThreadLocal对象是一个弱引用对象,也就是说每当发生GC,ThreadLocal对象就会被回收。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们来看一下ThreadLocal中的set(T value)
方法的具体实现。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
首先是获取当前线程,根据当前线程拿到threadLocals
,如果这个变量存在就为它设置一个值,否则就初始化threadLocals
。整体的思路我们通过流程图来捋一下。
总结一下上面说的内容,Thread类维护着一个ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的静态内部类并且ThreadLocalMap中的Entry的key是ThreadLocal对象的弱引用,value是要存储的内容,大致是下面这样的关系。
根据上面的内容我们再看一下get()
的方法体。
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
看看get()
方法是不是豁然开朗?同样是获取当前线程,然后通过线程拿到ThreadLocalMap对象,再通过ThreadLocalMap对象获取存储的值。当然如果ThreadLocalMap对象不存在那就去初始化一下。
ThreadLocal是怎么内存泄漏的
由于ThreadLocalMap中的key是ThreadLocal的弱引用,一旦发生GC便会回收ThreadLocal,那么此时的ThreadLocalMap存储的key便是null。如果不通过手动remove()
那么ThreadLocalMap的Entry便伴随线程的整个生命周期造成内存泄漏,大致就是一个thread ref -> thread -> threadLocals -> entry -> value
的强引用关系。因此Java其实是有对于内存泄漏的一些预防机制的,每次调用ThreadLocal的set()
、get()
、remove()
方法时都会回收key为空的Entry的value。
那么为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove()
,那么key会和value一样伴随线程的整个生命周期,如果key是弱引用,被GC后至少ThreadLocal被回收了,在下一次的set()
、get()
、remove()
还会回收key为null的Entry的value。
喜欢本文的朋友欢迎关注我的公众号:SKY技术修炼指南