> 文章列表 > ThreadLocal原理详解

ThreadLocal原理详解

ThreadLocal原理详解

1.概述

我们知道,想要实现变量共享,可以采取定义static修饰的类变量(静态变量)的形式,所有的线程共享这个变量。但是现在,我们想要实现每个线程独享这个变量,应该怎么实现呢?

2.什么是ThreadLocal

于是乎,ThreadLocal出现了。它实现了线程与线程之间的数据的隔离,互不干扰。那么ThreadLocal到底是何方神圣呢?

ThreadLocal是用来管理线程的私有数据,使得线程对该数据的操作对其他线程不可见,达到数据隔离的效果。

那么,Java中是怎么实现这种机制的呢?接下来一一揭晓。

2.1 Thread、ThreadLocal、ThreadLocalMap三剑客

ThreadLocal中定义了内部类ThreadLocalMapThreadLocalMap类似于HashMap,其中keyThreadLocalvalue为想要存储的值。然后呢,每个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对象

ThreadLocal原理详解
再来看看它们的数据结构:

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());}
}

ThreadLocal原理详解

接下来我们来验证一下数据隔离的效果。

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();}
}

执行结果:

ThreadLocal原理详解

从结果中可知,虽然我们营造了一个李四在张三之后进行签名的场景,但是李四签名之后并没有覆盖掉张三的签名,说明,张三和李四之间的行为是互不影响的,也就是所谓的数据隔离。

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();}}
}

执行结果:

我们看到,并没有按照预期打印出正确的结果,相反还报错了,仅打印出了一个时间,还是个错误的时间,这说明在多线程环境下出现了线程安全的问题。

ThreadLocal原理详解

那么为什么会出现线程安全问题呢?我们来看看SimpleDateFormat的源码。

ThreadLocal原理详解

来看这段官方文档,表明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原理详解

5.源码分析

前面我们在初体验ThreadLocal的API时,就已得知,第一次调用ThreadLocalget()方法时,返回值为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出现的原因就是为了能够为每个线程提供属于自己的变量,来达到数据隔离的效果。