ThreadLocal原理详解
1.概述
我们知道,想要实现变量共享,可以采取定义static修饰的类变量(静态变量)的形式,所有的线程共享这个变量。但是现在,我们想要实现每个线程独享这个变量,应该怎么实现呢?
2.什么是ThreadLocal
于是乎,ThreadLocal
出现了。它实现了线程与线程之间的数据的隔离,互不干扰。那么ThreadLocal
到底是何方神圣呢?
ThreadLocal是用来管理线程的私有数据,使得线程对该数据的操作对其他线程不可见,达到数据隔离的效果。
那么,Java中是怎么实现这种机制的呢?接下来一一揭晓。
2.1 Thread、ThreadLocal、ThreadLocalMap三剑客
ThreadLocal
中定义了内部类ThreadLocalMap
,ThreadLocalMap
类似于HashMap
,其中key
为ThreadLocal
,value
为想要存储的值。然后呢,每个Thread
对象中维护了一个ThreadLocalMap
,因此,每个线程都可以很方便的访问只属于自己的数据。
我们通过源代码来跟踪一下Thread、ThreadLocal、ThreadLocalMap三者之间的联系。
ThreadLocal.java
// ThreadLocal.java
public class ThreadLocal {// 静态内部类static class ThreadLocalMap {}
}
Thread.java
// Thread.java
public class Thread {ThreadLocal.ThreadLocalMap threadLocals = null;
}
那么,它们三者的关系呢就像下面这张图一样:
- 人(Thread):当前线程对象。
- 公文包(ThreadLocalMap):属于当前线程对象,用于存放只能由当前线程访问的数据。
- 文件(ThreadLocal):文件的key为ThreadLocal对象
再来看看它们的数据结构:
3.ThreadLocal的作用
先来体验一下ThreadLocal
的API:
public class ThreadLocalTest {private static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {System.out.println(threadLocal.get());threadLocal.set("one and only");System.out.println(threadLocal.get());}
}
接下来我们来验证一下数据隔离的效果。
import lombok.SneakyThrows;import java.util.concurrent.TimeUnit;public class ThreadLocalTest {private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();@SneakyThrowspublic static void main(String[] args) {Thread threadA = new Thread(() -> {threadLocal.set(Thread.currentThread().getName());System.out.println(Thread.currentThread().getName() + "完成了签名");try {// 睡眠2秒,以保证李四线程先执行完成TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "查看了自己的数据,签名:" + threadLocal.get());}, "张三");Thread threadB = new Thread(() -> {threadLocal.set(Thread.currentThread().getName());System.out.println(Thread.currentThread().getName() + "完成了签名");System.out.println(Thread.currentThread().getName() + "查看了自己的数据,签名:" + threadLocal.get());}, "李四");threadA.start();// 先启动张三线程,再启动李四线程TimeUnit.SECONDS.sleep(1);threadB.start();// 释放内存,防止造成内存泄漏threadLocal.remove();}
}
执行结果:
从结果中可知,虽然我们营造了一个李四在张三之后进行签名的场景,但是李四签名之后并没有覆盖掉张三的签名,说明,张三和李四之间的行为是互不影响的,也就是所谓的数据隔离。
4.ThreadLocal实战场景
我们先来看一个多线程安全的问题,来自于日期格式化类SimpleDateFormat
,代码如下:
public class ThreadLocalTest {private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");private static String[] dateArray = {"2023-01-01 09:00:00", "2023-02-01 10:00:00", "2023-03-01 11:00:00"};public static void main(String[] args) {for (int i = 0; i < 3; i++) {int finalI = i;new Thread(() -> {try {Date date = simpleDateFormat.parse(dateArray[finalI]);System.out.println(simpleDateFormat.format(date));} catch (ParseException e) {e.printStackTrace();}}).start();}}
}
执行结果:
我们看到,并没有按照预期打印出正确的结果,相反还报错了,仅打印出了一个时间,还是个错误的时间,这说明在多线程环境下出现了线程安全的问题。
那么为什么会出现线程安全问题呢?我们来看看SimpleDateFormat
的源码。
来看这段官方文档,表明SimpleDateFormat
是不同步的,建议为每个线程创建一个单独的SimpleDateFormat
实例,这不正符合ThreadLocal
的使用场景吗,或者在多线程环境下使用线程同步。
我们这里可以采用ThreadLocal
来为每个线程创建SimpleDateFormat
实例的方式来解决线程安全的问题。
public class ThreadLocalTest {private static String[] dateArray = {"2023-01-01 09:00:00", "2023-02-01 10:00:00", "2023-03-01 11:00:00"};// 前面我们知道直接调用get(),返回的值为null,因此我们需要提前设置初始化值private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));public static void main(String[] args) {for (int i = 0; i < 3; i++) {int finalI = i;new Thread(() -> {SimpleDateFormat simpleDateFormat = threadLocal.get();try {Date date = simpleDateFormat.parse(dateArray[finalI]);System.out.println(simpleDateFormat.format(date));} catch (ParseException e) {e.printStackTrace();}}).start();}}
}
执行结果:
这次结果打印正常了,由于线程执行的随机性,所以打印顺序也是随机的。
5.源码分析
前面我们在初体验ThreadLocal
的API时,就已得知,第一次调用ThreadLocal
的get()
方法时,返回值为null,因为它并没有一个初始化值,因此我们需要先为每个线程设置一个初始化值,这些都需要我们去跟踪源码进行一一分析。
源码分析环节,我们从以下几个方面进行:
- Thread.get()
- 设置初始化值
- Thread.set()
- ThreadLocalMap
5.1 ThreadLocal.get()
先来看看get()
的源码:
// ThreadLocal.javapublic T get() {// 1.获取当前线程Thread t = Thread.currentThread();// 2.获取当前线程对象独有的ThreadLocalMapThreadLocalMap map = getMap(t);// 3.如果ThreadLocalMap存在,且存在key,则返回值,否则设置初始化值if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 4.如果无法从如果ThreadLocalMap获取值,则设置初始化值。return setInitialValue();
}
因为我们是直接调用get()
方法,所以此时ThreadLocalMap
中是没有值的,因此走到setInitialValue()
。
// ThreadLocal.javaprivate T setInitialValue() {// 初始化值T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);}return value;
}
这点重点关注initialValue()
方法。
protected T initialValue() {return null;
}
出乎意料,这里竟然直接返回null,这就涉及到我们的第二点为线程的独立变量设置初始化值。
5.2 设置初始化值
在分析get()
方法时,我们得知initialValue()
的返回值为null,要设置初始化值,我们可以继承ThreadLocal并覆写initialValue()
即可。
public class CustomThreadLocal extends ThreadLocal<SimpleDateFormat> {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}
}
还有一种写法,我们在实战场景部分已经使用过了,代码如下:
private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
主要借助了ThreadLocal
的一个内部类SuppliedThreadLocal
。
先来看withInitial()
方法,参数是Supplier
接口,这也是很常见的函数式接口。方法的内部实际上也是返回了ThreadLocal
的子类SuppliedThreadLocal
。这种写法与我们的第一种写法的原理是一样的,免去我们再手动创建一个子类。
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {return new SuppliedThreadLocal<>(supplier);
}
再来看SuppliedThreadLocal
,得知,我们只需要在提供的Supplier
中定义好初始化独立变量的逻辑即可。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {private final Supplier<? extends T> supplier;SuppliedThreadLocal(Supplier<? extends T> supplier) {this.supplier = Objects.requireNonNull(supplier);}@Overrideprotected T initialValue() {return supplier.get();}
}
5.3 ThreadLocal.set()
接下来我们来看看set()
方法。
// ThreadLocal.javapublic void set(T value) {// 1.获取当前线程Thread t = Thread.currentThread();// 2.获取当前线程对象独有的ThreadLocalMapThreadLocalMap map = getMap(t);// 3.如果ThreadLocalMap存在就设置新值,否则就创建一个新值并存储到线程中if (map != null) {map.set(this, value);} else {createMap(t, value);}
}// 返回线程对象的threadLocals变量
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}// 创建一个新的ThreadLocalMap并设置值
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这里我们重点关注的就是ThreadLocalMap
了,它是用来存储数据的地方。
5.4 ThreadLocalMap
这里我们贴出关键的代码。
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
}
不难发现,它的结构类似于HashMap
,但它的Entry
是继承自WeakReference<ThreadLocal<?>>
,因此,它的key是一个弱引用。
我们来回顾一下四大引用引用的特征:
强引用(StrongLyReference)
传统意义上的引用,类似于Object obj = new Object();
只要强引用关系存在,垃圾收集器就不会回收这些对象。
软引用(SoftReference)
描述一些还有用,但非必需的对象。
只被软引用关联着的对象,垃圾收集时,如果内存空间足够,则不会收集这些对象;一旦内存空间不足,才会收集这些对象。
弱引用(WeakReference)
描述一些还有用,但非必需的对象,垃圾收集时,不管内存空间是否足够,都会收集这些对象。
虚引用(PhantomReference)
形同虚设,并不会影响对象的生命周期。
垃圾收集时,会将虚引用放入队列,就可以得知改对象被收集了。
这里我们再探讨一个问题,那就是ThreadLocal的内存泄漏问题,那么为什么会出现内存泄漏呢?
// Thread.java
public class Thread {ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap
对象与Thread
线程对象的生命周期是一样的,因此即使ThreadLocalMap
的key是弱引用,因此在垃圾回收时是可以被收集的,那么可能的原因就是value值无法被回收。这就是我们需要注意的点,在使用ThreadLocal
时,使用完毕后,配合调用remove()
,就会清除掉ThreadLocalMap
中key为null的value。
6.总结
综上所述,ThreadLocal出现的原因就是为了能够为每个线程提供属于自己的变量,来达到数据隔离的效果。