多线程访问共享可变数据时,涉及到线程间数据同步问题。然而,并不是所有时候都需要共享数据,所以,线程封闭的概念就提出来了。
通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
线程封闭的具体体现有:
局部变量位于执行线程的栈中,其他线程无法访问这个栈。线程封闭是局部变量的固有属性。
java.lang.ThreadLocal
,顾名思义,它可以存放线程本地变量。ThreadLocal
让每个线程维护变量的一个副本,各线程通过ThreadLocal
去访问该变量时会拿到各自的副本,副本之间相互独立,互不影响,这样竞争条件被彻底消除了。
下面通过一个例子来验证ThreadLocal
的特性。
public class ThreadLocalTest {
private static final ThreadLocal<String> value = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
value.set("主线程设置的123");
System.out.println("线程1执行之前,主线程取到的值: " + value.get());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程1取到的值: " + value.get());
value.set("线程1设置的值456");
System.out.println("重新设置后线程1取到的值: " + value.get());
System.out.println("线程1执行结束");
} finally {
value.remove();
}
}
}, "线程1");
thread.start();
// 等待线程1执行结束
thread.join();
System.out.println("线程1执行之后,主线程取到的值: " + value.get());
value.remove();
}
}
这段程序的输出是:
线程1执行之前,主线程取到的值: 主线程设置的123
线程1取到的值: null
重新设置后线程1取到的值: 线程1设置的值456
线程1执行结束
线程1执行之后,主线程取到的值: 主线程设置的123
可以看出,不同的线程通过ThreadLocal
进行变量的读写时,是互不干扰的。
ThreadLocal
这么神奇,它到底是怎么实现的呢?
ThreadLocal
有3个核心方法:
get()
set()
remove()
这里主要看get()
方法 。
public T get() {
// 拿到当前线程对应的ThreadLocalMap对象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 从map中查询对应的变量副本
if (map != null) {
// 以ThreadLocal对象为key,从map中获取ThreadLocalMap.Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果entry不为空,entry的value就是目标变量副本
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 否则,初始化变量副本
return setInitialValue();
}
从get()
方法中可以看出,我们希望得到的变量副本存放在ThreadLocalMap
中。而ThreadLocalMap
是和线程绑定的:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
在Thread
类里,有这样一个属性:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap
的结构如下:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
ThreadLocalMap
是一个哈希表,它里面存放若干个指向ThreadLocal
对象的弱引用,而我们需要的value值就挂靠在这个弱引用上。因此,根据ThreadLocal
找到对应的Entry
就能拿到目标变量的副本。
这里使用弱引用的目的是希望在
ThreadLocal
对象被回收后可以自动回收value对象。
接下来看get()
方法里的第二个分支,setInitialValue()
。进入这个分支说明当前线程对应的ThreadLocalMap
还未初始化,或者ThreadLocalMap
里面还没有初始化ThreadLocal
对象对应的Entry
。
private T setInitialValue() {
// 获取初始值(变量副本)
T value = initialValue();
// 获取当前线程对应的ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap已经初始化,则将ThreadLocal对象和变量副本的映射关系保存在map中
if (map != null)
map.set(this, value);
// 否则,初始化ThreadLocalMap,并保存ThreadLocal对象和变量副本的映射关系
else
createMap(t, value);
// 返回变量副本的值
return value;
}
其中,initialValue()
的实现是:
protected T initialValue() {
return null;
}
这是一个protected
方法,默认返回null
值。这意味着,对于一个ThreadLocal
对象,线程访问它拿到的默认变量副本是null
(这也解释了在前面的示例中线程1一开始拿到的是null
值)。我们可以覆盖这个方法,指定一个默认的变量副本,这样可以省去调用get()
方法时的一次非空判断。ThreadLocal
类里有一个静态内部类SuppliedThreadLocal
,它已经帮我们覆盖了默认的initialValue()
方法,只需要使用ThreadLocal
的静态方法ThreadLocal#withInitial
就可以在创建ThreadLocal
对象时轻松指定默认值。
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
到这里,我们对get()
方法的有了大致的了解:获取当前线程的ThreadLocalMap
对象,在ThreadLocalMap
里以ThreadLocal
对象为Key查询Entry
,Entry
对应的value就是我们希望得到的变量副本。如果查找失败,就初始化变量副本(还可能初始化ThreadLocalMap
),并存入ThreadLocalMap
里,再将变量副本返回给调用者。
ThreadLocal
与使用它的Thread
紧密相连:
Thread
有且仅有一个ThreadLocalMap
对象。ThreadLocalMap
对象存储多个Entry
对象。Entry
对象的key的弱引用指向一个ThreadLocal
对象。ThreadLocal
对象被多个线程所共享。ThreadLocal
对象不持有value,value由线程的Entry
对象持有。了解了get()
的实现逻辑,set()
和remove()
方法就不难理解了,这里不再展开。
ThreadLocal
的主要问题是会产生脏数据和内存泄漏。这两个问题通常是在线程池中使用ThreadLocal
引发的,因为线程池有线程复用和内存常驻两个特点。
线程复用会产生脏数据。由于线程池会重用Thread
对象,那么与Thread
绑定的ThreadLocalMap
变量也会被重用。如果在实现的线程的run()
方法中不显式的调用remove()
清理与线程相关的ThreadLocal
信息,那么倘若下一个任务不调用set()
设置初始值,就有可能get()
到重用的线程信息,包括ThreadLocal
所关联的线程对象的value值。
通常使用static
关键字来修饰ThreadLocal
,在此场景下,寄希望于ThreadLocal
对象失去引用后,触发弱引用机制来回收Entry
的value就不现实了。如果不进行remove()
操作,那么ThreadLocal
对象持有的value是不会被释放的。
以上两个问题解决办法很简单,就是在每次用完ThreadLocal
时,必须及时调用remove()
方法清理。