线程封闭与ThreadLocal
多线程访问共享可变数据时,涉及到线程间数据同步问题。然而,并不是所有时候都需要共享数据,所以,线程封闭的概念就提出来了。
通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
线程封闭的具体体现有:
- 局部变量
- ThreadLocal
局部变量
局部变量位于执行线程的栈中,其他线程无法访问这个栈。线程封闭是局部变量的固有属性。
ThreadLocal
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()
方法清理。