我们知道,想要实现变量共享,可以采取定义static修饰的类变量(静态变量)的形式,所有的线程共享这个变量。但是现在,我们想要实现每个线程独享这个变量,应该怎么实现呢?
于是乎,ThreadLocal
出现了。它实现了线程与线程之间的数据的隔离,互不干扰。那么ThreadLocal
到底是何方神圣呢?
ThreadLocal是用来管理线程的私有数据,使得线程对该数据的操作对其他线程不可见,达到数据隔离的效果。
那么,Java中是怎么实现这种机制的呢?接下来一一揭晓。
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;
}
那么,它们三者的关系呢就像下面这张图一样:
先来体验一下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<>();
@SneakyThrows
public 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();
}
}
执行结果:
从结果中可知,虽然我们营造了一个李四在张三之后进行签名的场景,但是李四签名之后并没有覆盖掉张三的签名,说明,张三和李四之间的行为是互不影响的,也就是所谓的数据隔离。
我们先来看一个多线程安全的问题,来自于日期格式化类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();
}
}
}
执行结果:
这次结果打印正常了,由于线程执行的随机性,所以打印顺序也是随机的。
前面我们在初体验ThreadLocal
的API时,就已得知,第一次调用ThreadLocal
的get()
方法时,返回值为null,因为它并没有一个初始化值,因此我们需要先为每个线程设置一个初始化值,这些都需要我们去跟踪源码进行一一分析。
源码分析环节,我们从以下几个方面进行:
先来看看get()
的源码:
// ThreadLocal.java
public T get() {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.获取当前线程对象独有的ThreadLocalMap
ThreadLocalMap 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.java
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);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
这点重点关注initialValue()
方法。
protected T initialValue() {
return null;
}
出乎意料,这里竟然直接返回null,这就涉及到我们的第二点为线程的独立变量设置初始化值。
在分析get()
方法时,我们得知initialValue()
的返回值为null,要设置初始化值,我们可以继承ThreadLocal并覆写initialValue()
即可。
public class CustomThreadLocal extends ThreadLocal<SimpleDateFormat> {
@Override
protected 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);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
接下来我们来看看set()
方法。
// ThreadLocal.java
public void set(T value) {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.获取当前线程对象独有的ThreadLocalMap
ThreadLocalMap 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
了,它是用来存储数据的地方。
这里我们贴出关键的代码。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
不难发现,它的结构类似于HashMap
,但它的Entry
是继承自WeakReference
,因此,它的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。
综上所述,ThreadLocal出现的原因就是为了能够为每个线程提供属于自己的变量,来达到数据隔离的效果。