Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
在解答本文的标题问题之前,先要搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系。
首先我们梳理下它们的定义与作用:
Thread(线程)
-
定义:Thread 是 Java 中的线程类,它代表了一个单独的执行单元。每个线程都有自己的执行栈和局部变量。
-
作用:Thread 用于创建和管理线程,使程序能够并发执行。
ThreadLocal(线程本地变量)
-
定义:ThreadLocal 是一个用于在多线程环境中为每个线程提供独立变量副本的类。每个线程都可以独立地设置和获取自己的变量副本,而不会与其他线程的变量副本发生冲突。
-
作用:ThreadLocal 用于实现线程安全的变量访问,避免了使用同步机制的开销。
ThreadLocalMap(线程本地映射)
-
定义:ThreadLocalMap 是 ThreadLocal 的一个内部静态类,用于存储每个线程的线程本地变量的键值对。它是一个哈希表,键是 ThreadLocal 对象,值是线程本地变量的值。
-
作用:ThreadLocalMap 用于在每个线程中维护线程本地变量的映射关系,使得每个线程可以独立地访问自己的变量副本。
三者之间的关系
下面我们用最直观、最容易理解的图画的方式来看看它们三者的关系:
我们看到最左下角的 Thread 1,这是一个线程,它的箭头指向了 ThreadLocalMap 1,其要表达的意思是,每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,在这里 Thread 1 所拥有的成员变量就是 ThreadLocalMap 1。
而这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。那么我们就来看一下它的 key 和 value 分别是什么。可以看到这个表格的左侧是 ThreadLocal 1、ThreadLocal 2…… ThreadLocal n,能看出这里的 key 就是 ThreadLocal 的引用。
而在表格的右侧是一个一个的 value,这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等。
这里需要重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map的数据结构来存放 ThreadLocal 和 value。
通过这张图片,我们就可以搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者在宏观上的关系了。
Thread >ThreadLocalMap>ThreadLocal :
每个 Thread 对象内部都有一个 ThreadLocalMap 类型的成员变量,用于存储该线程的线程本地变量映射。
ThreadLocalMap 是 ThreadLocal 的载体,每个线程通过 ThreadLocalMap 来管理自己的线程本地变量。
ThreadLocal 操作 ThreadLocalMap:
ThreadLocal 提供了 get、set、remove 等方法,这些方法实际上是对 ThreadLocalMap 进行操作。
当一个线程调用 ThreadLocal.set(value) 方法时,实际上是在该线程的 ThreadLocalMap 中存储一个键值对,键是当前的 ThreadLocal 对象,值是传入的 value。
当一个线程调用 ThreadLocal.get() 方法时,实际上是从该线程的 ThreadLocalMap 中获取与当前 ThreadLocal 对象关联的值。
ThreadLocalMap 的生命周期:
ThreadLocalMap 的生命周期与 Thread 对象的生命周期绑定。当一个线程结束时,其对应的 ThreadLocalMap 也会被销毁。
ThreadLocalMap 是线程私有的,每个线程都有自己的 ThreadLocalMap,不会与其他线程共享。
源码分析
知道了它们的关系之后,我们再来进行源码分析,来进一步地看到它们内部的实现。
我们先从ThreadLocal 开始,对于ThreadLocal我们通过前面的ThreadLocal 是用来解决共享资源的多线程访问的问题吗?和ThreadLocal 适合用在哪些实际生产的场景中?已经有了不错的了解,我们下面从它的一些具体方法去看看。
ThreadLocal的get 方法
我们来看一下 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 initialValue method.
翻译:
返回此线程局部变量在当前线程副本中的值。如果该变量对于当前线程没有值,那么它首先会被初始化为调用 `initialValue` 方法所返回的值。
源码代码如下:
/*** 获取当前线程中此线程局部变量副本的值。* 如果当前线程的该变量还没有值,会首先调用 {@link #initialValue} 方法对其进行初始化。** @return 当前线程中此线程局部变量的值*/public T get() {// 获取当前正在执行的线程Thread t = Thread.currentThread();// 从当前线程中获取对应的 ThreadLocalMap,该 map 存储了线程局部变量ThreadLocalMap map = getMap(t);// 检查当前线程的 ThreadLocalMap 是否存在if (map != null) {// 从 ThreadLocalMap 中获取与当前 ThreadLocal 对象关联的条目ThreadLocalMap.Entry e = map.getEntry(this);// 检查获取的条目是否存在if (e != null) {// 抑制编译器对类型转换的警告,因为这里可以确定值的类型@SuppressWarnings("unchecked")// 从条目中提取值并转换为泛型类型 TT result = (T)e.value;// 返回获取到的值return result;}}// 如果 ThreadLocalMap 不存在或者条目不存在,调用 setInitialValue 方法设置初始值并返回return setInitialValue();}
这是 ThreadLocal 的 get 方法,可以看出它利用了 Thread.currentThread 来获取当前线程的引用,并且把这个引用传入到了 getMap 方法里面,来拿到当前线程的 ThreadLocalMap。
然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。
值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。
ThreadLocal的getMap 方法
下面我们来看一下 getMap 方法,源码如下所示:
/*** 获取与指定线程关联的 ThreadLocalMap。* 该方法用于获取指定线程的 ThreadLocalMap 实例,* 该实例存储了线程局部变量的映射关系。** @param t 当前线程* @return 与指定线程关联的 ThreadLocalMap,如果线程没有关联的 ThreadLocalMap,则返回 null。*/ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
可以看到,这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null,代码如下:
/*** 与当前线程相关的ThreadLocal值。这个映射由ThreadLocal类维护。* 每个线程都有自己独立的ThreadLocalMap实例,用于存储该线程的ThreadLocal变量的值。* 当线程使用ThreadLocal对象存储值时,这些值实际上是存储在该线程的ThreadLocalMap中。*/ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal的set 方法
下面我们再来看一下 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 initialValue method to set the values of thread-locals.
翻译:
将此线程局部变量的当前线程副本设置为指定值。大多数子类无需重写此方法,仅依靠 initialValue 方法来设置线程局部变量的值即可。
源码代码如下:
/*** 设置当前线程的此线程局部变量的副本为指定的值。* 大多数子类无需重写此方法,仅依赖 {@link #initialValue} 方法来设置线程局部变量的值。** @param value 要存储在此线程局部变量的当前线程副本中的值。*/public void set(T value) {// 获取当前正在执行的线程Thread t = Thread.currentThread();// 获取当前线程关联的 ThreadLocalMapThreadLocalMap map = getMap(t);// 若该线程的 ThreadLocalMap 已存在if (map != null)// 将当前 ThreadLocal 对象作为键,指定的值作为值,存入 ThreadLocalMapmap.set(this, value);else// 若 ThreadLocalMap 不存在,则为当前线程创建一个新的 ThreadLocalMap,并将初始值存入createMap(t, value);}
set 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。
可以看出,map.set(this, value) 传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。
ThreadLocalMap 类
下面我们来看一下 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.
翻译:
ThreadLocalMap是一种定制的哈希映射,仅适用于维护线程局部值。没有任何操作会导出到ThreadLocal类之外。该类是包私有类,目的是允许在Thread类中声明字段。为了便于处理非常大且长期存在的使用场景,哈希表条目对键使用弱引用。然而,由于未使用引用队列,只有当表开始空间不足时,过期条目才一定会被移除。
通过前面我们知道,它就是 Thread.threadLocals,在ThreadLocalMap中的Entry类的源码注释如下:
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.
翻译:
此哈希映射中的条目扩展自WeakReference,将其main ref字段用作键(该键始终是一个ThreadLocal对象)。请注意,空键(即entry.get() == null)意味着该键不再被引用,因此该条目可以从表中清除。在后续代码中,此类条目被称为“过期条目”。
下面我们看看它的实现源码:
/*** 该类继承自 WeakReference,用于表示 ThreadLocalMap 中的条目。* 每个条目包含一个对 ThreadLocal 对象的弱引用和与之关联的值。* 弱引用的使用允许在没有其他强引用指向 ThreadLocal 对象时,该对象可以被垃圾回收。*/static class Entry extends WeakReference<ThreadLocal<?>> {/** 与该 ThreadLocal 关联的值。 */Object value;/*** 构造一个新的 Entry 对象。** @param k 与该条目关联的 ThreadLocal 对象。* @param v 与该 ThreadLocal 对象关联的值。*/Entry(ThreadLocal<?> k, Object v) {// 调用父类 WeakReference 的构造函数,传入 ThreadLocal 对象super(k);// 初始化与该 ThreadLocal 关联的值value = v;}}/*** 存储线程本地变量条目的数组,根据需要进行调整大小。* 数组的长度必须始终是 2 的幂次方,以确保哈希计算的一致性。*/private Entry[] table;
ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,其中最重要的就是截取出的这段代码中的 Entry 内部类。在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把Entry 理解为一个 map,其键值对为:
- 键,当前的 ThreadLocal;
- 值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。
ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。
但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同。
比如其中最重要的一个不同点就是 ThreadLocalMap 和 HashMap 在处理冲突时的区别。我们在为什么 Map 桶中超过 8 个才转为红黑树?有提到 HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链。但是 ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。