ThreadLocal 线程本地存储
并发场景下,多个线程同时读写共享变量就有可能产生并发安全问题。反过来也可以说,不存在共享变量,就不会出现线程安全问题。Java中有两种常用的避免共享变量的方法,使用局部变量,以及使用 ThreadLocal。
局部变量存在于每个线程内部的调用栈中,多个线程之间互相访问不到对方的局部变量,这就叫做线程封闭。如下图所示,局部变量存在于线程各自的调用栈中,线程之间互不打扰。
局部变量,线程封闭采用局部变量的方案,的确避免了变量被多个线程共享,同时它也禁止同一个线程中不同方法共享这个变量。然而,单线程中不同的方法之间共享变量是不会导致线程安全问题的。
如果想让同一个线程,不同的方法共享变量就可以使用 ThreadLocal,Java 提供的线程本地存储方案。ThreadLocal 可以保证同一个变量,该线程中的方法看到的值是一样,不同线程之间却是隔离。
ThreadLocal 的使用方法
常规使用 ThreadLocal 的方式很简单,创建一个 ThreadLocal 对象,然后调用它的 set(value)
方法设置值,再调用 get()
方法获取这个 ThreadLocal 对象对应的value。
// 创建一个 ThreadLocal
ThreadLocal<String> tl = new ThreadLocal<>();
// set方法
tl.set("深页");
// get方法
tl.get();
ThreadLocal 类的注释中还带有为每个线程分配自增 id 的示例代码。withInitial()
方法会调用initialValue()
方法,为 ThreadLocal 设置 get()
的初始值。执行下面的代码,可以看到每个类都有自己的id,并且id的自增的。
public class ThreadId {
// Integer类型的原子类,用来分配Id,保证其本身是线程安全的
private static final AtomicInteger nextId = new AtomicInteger();
// 创建一个 ThreadLocal 变零,并且为其赋值
private static ThreadLocal <Integer> threadId = ThreadLocal.withInitial(
() -> nextId.getAndIncrement()
);
// 获取id,即从ThreadLocal中获取对应的值
public static int getId() {
return threadId.get();
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(Thread.currentThread().getName()
+ ": " + ThreadId.getId()))
.start();
}
}
}
ThreadLocal还有一个经典的使用案例,就是将线程不安全的 SimpleDateFormat 类封装成线程安全的,原理其实和上面的例子是一样:
static class SafeDateFormat {
static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(
()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
static DateFormat get() {
return tl.get();
}
}
ThreadLocal的底层原理
先来看 set()
方法:
- 首先获取当前线程,然后通过当前线程获取线程持有的局部变量 threadLocals
- 如果返回的 map 不是空的就设置值
- 如果返回的 map 是空的,就调用构造方法初始化 map 并为其设置值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
再看 get()
方法:
public T get() {
Thread t = Thread.currentThread();
// getMap()返回当前线程的threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
// map存在返回当前ThreadLocal对应的value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// map不存在就初始化
return setInitialValue();
}
看过上面两个方法,可以看到它们除了涉及到 Thread 类,还涉及到了一个类 ThreadLocalMap。那么 Thread、ThreadLocal、ThreadLocalMap 之间是什么关系呢?
ThreadLocal 的数据结构ThreadLocalMap 是 ThreadLocal 的静态内部类,ThreadLocalMap 的底层是一个 Entry[] table
数组,Entry 是 ThreadLocalMap 的静态内部类,以 ThreadLocal 作为 key,以设置的值作为 value,如下所示:
static class Entry extends WeakReference <ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Thread 持有一个 ThreadLocalMap 的引用 threadLocals
:
ThreadLocal.ThreadLocalMap threadLocals = null;
所以说,我们通过当前线程 Thread t
可以到 t
持有的 ThreadLocalMap,并且通过 ThreadLocal 对象返回其对应的 value。
ThreadLocal 内存泄露问题
使用 Thread.start()
方法是不会产生内存泄露的问题的,只有当我们在线程池中使用 ThreadLocal 才有可能产生内存泄露问题。
内存泄露的本质是长生命周期的对象,持有短生命周期对象。当短生命周期的对象使用结束之后,理应被垃圾回收器回收,但是它却被一个更长生命周期的对象引用。通过可达性分析算法,该短生命周期的对象被一个GC Root引用,理应被回收的它就无法被回收。
那为什么在线程池中使用ThreadLocal就可能发生内存泄露的问题呢?我们就从长生命周期的对象,持有短生命周期对象这个角度进行分析。
图片来自于网络线程池作为一种池化资源技术,目的是避免线程的频繁创建和销毁。一般来说,线程池中的线程生命周期都很长,是和应用程序同生共死的。这就意味着,被 Thread 持有的 ThreadLocalMap 一直都不会被回收。
ThreadLocalMap 底层是一个 Entry 数组,Entry是<ThreadLocal,value>对结构。 Entry 对 ThreadLocal 是弱引用(WeakReference),所以ThreadLocal 生命周期之后,是结束是可以被回收掉的。但是 Entry 对 value 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。
InheritableThreadLocal 与继承性
使用 ThreadLocal 还有这样一种需求,ThreadLocal 创建了线程变量 V,然后希望该线程创建的子线程也能访问到父线程的线程变量 V。
为此 Java 提供了 InheritableThreadLocal
来支持这种特性,InheritableThreadLocal 继承自 ThreadLocal,用法其实和 ThreadLocal 一样。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
最后做一个小结,多线程同时读写共享变量就有可能产生并发问题。一种解决并发问题的思路就是避免变量被共享。与之对应的技术有线程隔离(局部变量),以及线程本地存储ThreadLocal
。
相比于使用局部变量,ThreadLocal 存储的变量可以供线程中的方法共享,单线程对共享变量的读写必定是线程安全的。