1.1 什么是 ThreadLocal
ThreadLocal 简单理解 Thread
即线程,Local
即本地,结合起来理解就是 每个线程都是本地独有的
。在早期的计算机中不包含操作系统,从头到尾只执行一个程序,并且这个程序能访问计算中的所有资源,这对于计算机资源来说是一种浪费。要想充分发挥多处理器的强大计算能力,最简单的方式就是使用多线程。与串行程序相比,在并发程序中存在更多容易出错的地方。当访问共享数据时,通常需要使用同步来控制并发程序的访问。一种避免使用同步的方式就是让这部分共享数据变成不共享的,试想一下,如果只是在单个线程内对数据进行访问,那么就可以不用同步了,这种技术称为 线程封闭(Thread Confinement)
,它是实现线程安全最简单的方式之一。<br/> 当某个对象封闭在一个单个线程中时,这种用法会自动实现了线程安全,因为只有一个线程访问数据,从根本上避免了共享数据的线程安全问题,即使被封闭的对象本身不是线程安全的。要保证线程安全,并不是一定就需要同步,两者没有因果关系,同步只是保证共享数据征用时正确性的手段,如果一个方法本来就不涉及共享数据,那它就不需要任何同步措施去保证正确性。而维持线程封闭的一种规范用法就是使用 ThreadLoal,这个类能使当前线程中的某个值与保存的值关联起来。ThreadLocal 提供了 get()
与 set(T value)
等方法,set
方法为每个使用了该变量的线程都存有一份独立的副本,因此当我们调用 get
方法时总是返回由当前线程在调用 set
方法的时候设置的最新值。
1.2 ThreadLocal 的用法
接下来通过一个示例代码说明 ThreadLocal 的使用方式,该示例使用了三个不同的线程 Main Thread
、Thread-1
和 Thread-2
分别对同一个 ThreadLocal 对象中存储副本。
/**
* @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();}
}
<!– more –>
打印的输出结果如下所示:
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 的时候,就会向 Map 中添加一条映射记录,而当线程运行结束时,应该从 Map 中清除该条记录,那么就会存在如下问题:
- 因为新增线程或者线程执行完都要操作这个 Map,所以需要保证 Map 是线程安全的。虽然可以使用 JDK 提供的 ConcurrentHashMap 来保证线程安全,但是它还是要通过使用锁来保证线程安全的。
- 当一个线程运行结束时要及时移除 Map 中对应的记录,不然可能会发生 内存泄漏 问题。
由于存在锁的问题,所有最终 JDK 并没有采用这个方案,而是使用无锁的 ThreadLocal
。上述方案出现锁的原因是因为有两一个以上的线程同时访问同一个 Map 导致的。我们可以换一种思路来看这个问题,如果将这个 Map 由每个 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那样就不会存在线程安全的问题,也不会需要锁了,因为是每个线程自己独有的,其它线程根本看不到其它线程的 Map。这个方案如下图所示:
这个方案虽然不存在锁的问题,但是由于每个线程访问 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLoal 变量与具体存储实例的映射,如果我们不手动删除这些实例,可能会造成内存泄漏。
我们进入到 Thread 的源码内可以看到其内部定义了一个 ThreadLocalMap
成员变量,如下图所示:
ThreadLoalMap 类是一个类似 Map 的类,是 ThreadLocal 的内部类。它的 key 是 ThreadLocal,一个 ThreadLocalMap 可以存储多个 key(ThreadLocal),它的 value 就对应着在 ThreadLocal 存储的 value。因此我们可以看出:每一个 Thread 都对应自己的一个 ThreadLocalMap,而 ThreadLocalMap 可以存储多个以 ThreadLocal 为 key 的键值对。这里也解释了为什么我们使用多个线程访问同一个 ThreadLocal,然后 get 到的确是不同数值。
上面对 ThreadLocal 进行了一些解释,接下来我们看看 ThreadLocal 具体是如何实现的。先看一下 ThreadLocal 类提供的几个常用方法:
protected T initialValue() { ...}
public void set(T value) {...}
public T get() { ...}
public void remove() { ...}
-
initialValue
方法是一个 protected 方法,一般是用来使用时进行重写,设置默认初始值的方法,它是一个延迟加载的方法,在。 -
set
方法是用来设置当前线程的变量副本的方法 -
get
方法是用获取 ThreadLocal 在当前线程中保存的变量副本 -
remove
方法是 JDK1.5+ 才提供的方法,是用来移除当前线程中的变量副本
initialValue
方法是在 setInitialValue
方法被调用的,由于 setInitialValue 方法是 private 方法,所以我们只能重写 initialValue 方法,我们看看 setInitialValue 的具体实现:
/**
* 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 方法中干了什么:
/**
* 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 到底是什么:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
它实际上就是一个 ThreadLocalMap,这个类型是 ThreadLocal 类内定义的一个内部类,我们看一下 ThreadLocalMap 的实现:
/**
* 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 方法的具体实现:
/**
* 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
方法的实现:
/**
* 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
方法的实现:
/**
* 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
设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。
- 通过 ThreadLocal 创建的副本是存储在每个线程自己的 threadLocals 变量中的
- 为何 threadLocals 的类型 ThreadLocalMap 的键值为 ThreadLocal 对象,因为每个线程中可有多个 threadLocal 变量,就像前文图片中的 ThreadLocal<String> 和 ThreadLocal<Integer>,就是一个线程存在两个 threadLocal 变量
- 在进行 get 之前,必须先 set,否则会报空指针异常,如果想在 get 之前不需要调用 set 就能正常访问的话,必须重写 initialValue 方法
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
另外,内存泄漏的问题请参考博文:ThreadLocal 内存泄漏问题