ThreadLocal 简单理解 Thread
即线程, Local
即本地,结合起来理解就是 每个线程都是本地独有的
。在早期的计算机中不包含操作系统,从头到尾只执行一个程序,并且这个程序能访问计算中的所有资源,这对于计算机资源来说是一种浪费。要想充分发挥多处理器的强大计算能力,最简单的方式就是使用多线程。与串行程序相比,在并发程序中存在更多容易出错的地方。当访问共享数据时,通常需要使用同步来控制并发程序的访问。一种避免使用同步的方式就是让这部分共享数据变成不共享的,试想一下,如果只是在单个线程内对数据进行访问,那么就可以不用同步了,这种技术称为 线程封闭(Thread Confinement)
,它是实现线程安全最简单的方式之一。<br/>当某个对象封闭在一个单个线程中时,这种用法会自动实现了线程安全,因为只有一个线程访问数据,从根本上避免了共享数据的线程安全问题,即使被封闭的对象本身不是线程安全的。要保证线程安全,并不是一定就需要同步,两者没有因果关系,同步只是保证共享数据征用时正确性的手段,如果一个方法本来就不涉及共享数据,那它就不需要任何同步措施去保证正确性。而维持线程封闭的一种规范用法就是使用 ThreadLoal ,这个类能使当前线程中的某个值与保存的值关联起来。ThreadLocal 提供了 get()
与 set(T value)
等方法, set
方法为每个使用了该变量的线程都存有一份独立的副本,因此当我们调用 get
方法时总是返回由当前线程在调用 set
方法的时候设置的最新值。
接下来通过一个示例代码说明 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 对象设置值,对不同的线程取出来的值不一样。接下来我们来分析一下源码,看看它是如何实现的。
既然要对每个访问 ThreadLocal 变量的线程都要有自己的一份 本地独立副本
。我们很容易想到可以用一个 Map 结构去存储,它的键就是我们当前的线程,值是它在该线程内的实例。然后当我们使用该 ThreadLocal 的 get 方法获取实例值时,只需要使用 Thread.currentThread()
获取当前线程,以当前线程为键,从我们的 Map 中获取对应的实例值即可。结构示意图如下所示:
上面这个方案可以满足前文所说的每个线程本地独立副本的要求。每个新的线程访问该 ThreadLocal 的时候,就会向 Map 中添加一条映射记录,而当线程运行结束时,应该从 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 set get remove
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 变量中查找了。
ThreadLocal
设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。
另外,内存泄漏的问题请参考博文: ThreadLocal 内存泄漏问题