Java 并发之 ThreadLocal

1.1 什么是 ThreadLocal

ThreadLocal 简单理解 Thread 即线程,Local 即本地,结合起来理解就是 每个线程都是本地独有的。在早期的计算机中不包含操作系统,从头到尾只执行一个程序,并且这个程序能访问计算中的所有资源,这对于计算机资源来说是一种浪费。要想充分发挥多处理器的强大计算能力,最简单的方式就是使用多线程。与串行程序相比,在并发程序中存在更多容易出错的地方。当访问共享数据时,通常需要使用同步来控制并发程序的访问。一种避免使用同步的方式就是让这部分共享数据变成不共享的,试想一下,如果只是在单个线程内对数据进行访问,那么就可以不用同步了,这种技术称为线程封闭(Thread Confinement),它是实现线程安全最简单的方式之一。
当某个对象封闭在一个单个线程中时,这种用法会自动实现了线程安全,因为只有一个线程访问数据,从根本上避免了共享数据的线程安全问题,即使被封闭的对象本身不是线程安全的。要保证线程安全,并不是一定就需要同步,两者没有因果关系,同步只是保证共享数据征用时正确性的手段,如果一个方法本来就不涉及共享数据,那它就不需要任何同步措施去保证正确性。而维持线程封闭的一种规范用法就是使用 ThreadLoal,这个类能使当前线程中的某个值与保存的值关联起来。ThreadLocal 提供了 get()set(T value) 等方法,set 方法为每个使用了该变量的线程都存有一份独立的副本,因此当我们调用 get 方法时总是返回由当前线程在调用 set 方法的时候设置的最新值。

1.2 ThreadLocal 的用法

接下来通过一个示例代码说明 ThreadLocal 的使用方式,该示例使用了三个不同的线程 Main ThreadThread-1Thread-2 分别对同一个 ThreadLocal 对象中存储副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @author mghio
* @date: 2019-10-20
* @version: 1.0
* @description: Java 并发之 ThreadLocal
* @since JDK 1.8
*/
public class ThreadLocalDemoTests {
private ThreadLocal<String> boolThreadLocal = ThreadLocal.withInitial(() -> "");

@Test
public void testUseCase() {
boolThreadLocal.set("main-thread-set");
System.out.printf("Main Thread: %s\n", boolThreadLocal.get());

new Thread("Thread-1") {
@Override
public void run() {
boolThreadLocal.set("thread-1-set");
System.out.printf("Thread-1: %s\n", boolThreadLocal.get());
}
}.start();

new Thread("Thread-2") {
@Override
public void run() {
System.out.printf("Thread-2: %s\n", boolThreadLocal.get());
}
}.start();
}
}

打印的输出结果如下所示:

1
2
3
Main Thread: main-thread-set
Thread-1: thread-1-set
Thread-2:

我们从输出结果可以看出,ThreadLocal 把不同的线程的数据进行隔离,互不影响,Thread-2 的线程因为我们没有重新设置值会使用 withInitial 方法设置的默认初始值 "",在不同的线程对同一个 ThreadLocal 对象设置值,对不同的线程取出来的值不一样。接下来我们来分析一下源码,看看它是如何实现的。

1.3 ThreadLocal 的实现原理

既然要对每个访问 ThreadLocal 变量的线程都要有自己的一份本地独立副本。我们很容易想到可以用一个 Map 结构去存储,它的键就是我们当前的线程,值是它在该线程内的实例。然后当我们使用该 ThreadLocal 的 get 方法获取实例值时,只需要使用 Thread.currentThread() 获取当前线程,以当前线程为键,从我们的 Map 中获取对应的实例值即可。结构示意图如下所示:
threadlocal-one.png
上面这个方案可以满足前文所说的每个线程本地独立副本的要求。每个新的线程访问该 ThreadLocal 的时候,就会向 Map 中添加一条映射记录,而当线程运行结束时,应该从 Map 中清除该条记录,那么就会存在如下问题:

  1. 因为新增线程或者线程执行完都要操作这个 Map,所以需要保证 Map 是线程安全的。虽然可以使用 JDK 提供的 ConcurrentHashMap 来保证线程安全,但是它还是要通过使用锁来保证线程安全的。
  2. 当一个线程运行结束时要及时移除 Map 中对应的记录,不然可能会发生 内存泄漏 问题。

由于存在锁的问题,所有最终 JDK 并没有采用这个方案,而是使用无锁的 ThreadLocal。上述方案出现锁的原因是因为有两一个以上的线程同时访问同一个 Map 导致的。我们可以换一种思路来看这个问题,如果将这个 Map 由每个 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那样就不会存在线程安全的问题,也不会需要锁了,因为是每个线程自己独有的,其它线程根本看不到其它线程的 Map 。这个方案如下图所示:
threalocal-two.png 这个方案虽然不存在锁的问题,但是由于每个线程访问 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLoal 变量与具体存储实例的映射,如果我们不手动删除这些实例,可能会造成内存泄漏。
我们进入到 Thread 的源码内可以看到其内部定义了一个 ThreadLocalMap 成员变量,如下图所示:
thread-codesource.png ThreadLoalMap 类是一个类似 Map 的类,是 ThreadLocal 的内部类。它的 key 是 ThreadLocal ,一个 ThreadLocalMap 可以存储多个 key(ThreadLocal),它的 value 就对应着在 ThreadLocal 存储的 value。因此我们可以看出:每一个 Thread 都对应自己的一个 ThreadLocalMap ,而 ThreadLocalMap 可以存储多个以 ThreadLocal 为 key 的键值对。这里也解释了为什么我们使用多个线程访问同一个 ThreadLocal ,然后 get 到的确是不同数值。

上面对 ThreadLocal 进行了一些解释,接下来我们看看 ThreadLocal 具体是如何实现的。先看一下 ThreadLocal 类提供的几个常用方法:

1
2
3
4
5
6
7
protected T initialValue() { ... }

public void set(T value) { ... }

public T get() { ... }

public void remove() { ... }
  1. initialValue 方法是一个 protected 方法,一般是用来使用时进行重写,设置默认初始值的方法,它是一个延迟加载的方法,在。
  2. set 方法是用来设置当前线程的变量副本的方法
  3. get 方法是用获取 ThreadLocal 在当前线程中保存的变量副本
  4. remove 方法是 JDK1.5+ 才提供的方法,是用来移除当前线程中的变量副本

initialValue 方法是在 setInitialValue 方法被调用的,由于 setInitialValue 方法是 private 方法,所以我们只能重写 initialValue 方法,我们看看 setInitialValue 的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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);
return value;
}

通过以上代码我们知道,会先调用 initialValue 获取初始值,然后使用当前线程从 Map 中获取线程对应 ThreadLocalMap,如果 map 不为 null,就设置键值对,如果为 null,就再创建一个 Map。
首先我们看下在 getMap 方法中干了什么:

1
2
3
4
5
6
7
8
9
10
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

可能大家没有想到的是,在 getMap 方法中,是调用当期线程 t,返回当前线程 t 中的一个成员变量 threadLocals 。那么我们继续到 Thread 类中源代码中看一下成员变量 threadLocals 到底是什么:

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

它实际上就是一个 ThreadLocalMap ,这个类型是 ThreadLocal 类内定义的一个内部类,我们看一下 ThreadLocalMap 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

...

}

我们可以看到 ThreadLocalMap 的 Entry 继承了 WeakReference (弱引用),并且使用 ThreadLocal 作为键值。

下面我们看下 createMap 方法的具体实现:

1
2
3
4
5
6
7
8
9
10
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

直接 new 一个 ThreadLoalMap 对象,然后赋值给当前线程的 threadLocals 属性。

然后我们看一下 set 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

首先获取当前线程,然后从线程的属性 threadLocals 获取当前线程对应的 ThreadLocalMap 对象,如果不为空,就以 this (ThreadLocal) 而不是当前线程 t 为 key,添加到 ThreadLocalMap 中。如果为空,那么就先创建后再加入。ThreadLocal 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏。

接下来我们看一下 get 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

先获取当前线程,然后通过 getMap 方法传入当前线程获取到 ThreadLocalMap 。然后接着获取 Entry (key,value) 键值对,这里传入的是 this,而不是当前线程 t ,如果获取成功,则返回对应的 value,如果没有获取到,返回空,则调用 setInitialValue 方法返回 value。

至此,我们总结一下 ThreadLocal 是如何为每个线程创建变量副本的:首先,在每个线程 Thread 内部有个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个 threadLocals 变量就是用来存储实际变量的副本的,它的键为当前 ThreadLocal ,value 为变量副本(即 T 类型的变量)。
初始时,在 Thread 类里面, threadLocals 为 null,当通过 ThreadLocal 调用 set 或者 get 方法时,如果此前没有对当前线程的 threadLocals 进行过初始化操作,那么就会以当前 ThreadLocal 变量为键值,以 ThreadLocal 要保存的副本变量为 value,存到当前线程的 threadLocals 变量中。以后在当前线程中,如果要用到当前线程的副本变量,就可以通过 get 方法在当前线程的 threadLocals 变量中查找了。

1.4 总结

ThreadLocal 设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。

  1. 通过 ThreadLocal 创建的副本是存储在每个线程自己的 threadLocals 变量中的
  2. 为何 threadLocals 的类型 ThreadLocalMap 的键值为 ThreadLocal 对象,因为每个线程中可有多个 threadLocal 变量,就像前文图片中的 ThreadLocal 和 ThreadLocal ,就是一个线程存在两个 threadLocal 变量
  3. 在进行 get 之前,必须先 set ,否则会报空指针异常,如果想在 get 之前不需要调用 set 就能正常访问的话,必须重写 initialValue 方法
  4. ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

另外,内存泄漏的问题请参考博文:ThreadLocal 内存泄漏问题

-------------本文结束感谢您的阅读-------------
mghio wechat
微信公众号「mghio」
赏作者☕️