ThreadLocal 类精讲
ThreadLocal 类精讲
文章目录
1. ThreadLocal 概述
ThreadLocal 类在多线程中很常见,其主要的特性可以概括为以下三点:
- 具备向所有线程提供上下文的能力;
- 延迟创建线程上下文实例;
- 一个线程可以绑定多个 ThreadLocal 实例;
- 同一个 ThreadLocal 可以被多个线程同时绑定;
- ThreadLocal 是一个彻底的工具类,本身不存储任何上下文信息;
2. 提供线程上下文能力
Context,上下文,其通常指能够提供环境、临时存储数据的实例。ThreadLocal 是一种上下文实例,Thread 可以通过调用其 set()
以及 get()
方法轻松地存取数据实例,如下代码案例所示:
public class Test {
public static void main(String[] args) {
/**
* 方式 1
*/
final ThreadLocal<String> threadLocal1 = new ThreadLocal();
threadLocal1.set("hello world");
/**
* 方式 2
*/
final ThreadLocal<Map<String,Long>> threadLocal2 = new ThreadLocal();
threadLocal2.set(new HashMap<String, Long>());
threadLocal2.get().put("k1",100000000000001L);
/**
* 方式 3
*/
final ThreadLocal<ThreadContext> contextThreadLocal = new ThreadLocal<>();
contextThreadLocal.set(new ThreadContext());
final ThreadContext threadContext = contextThreadLocal.get();
//利用 threadContext 进行一些数据存取工作
}
}
class ThreadContext{
}
这里的线程都是 main 线程。
- 方式 1 说明,每一个线程实例在同一个 ThreadLocal 实例中仅仅能够放置一个实例,类型是任意的。
- 方式 2 说明,通过放置的实例限制为 Map 类型,实际上我们能够存储很多数据;
- 方式 3 则是很多框架中使用的方式,提供一个额外的线程上下文类型,然后每个线程都将自身作为 key,额外的线程上下文实例(比如实例代码的 ThreadContext 类型)作为 value。存取数据都是再从 ThreadLocal 中得到线程对应的 ThreadContext 实例后,对 ThreadContext 实例进行存储属于。
Thread 的线程上下文可以是任意类型,因为事实上,Thread 并没有规定上下文的类型。上下文可以仅仅是一个 String,也可以是一个特殊的 Context 类型。
3. 线程上下文的延迟加载
ThreadLocal.set()
方法的源码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//得到当前 Thread 内部的 threadLocals 实例
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set()
方法的执行逻辑是:判断当前 Thread 实例内部的 threadLocals 有没有初始化(没有则是 null),如果没有则调用 createMap()
方法给当前线程的 threadLocals 进行初始化。如果已经初始化了,那么就将本地的 this-value 键值对覆盖掉。
Thread 类内部的 threadLocals 变量定义如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
从初始化方法
createMap()
方法可以知道,此 Map 的 key 为当前 ThreadLocal 实例,value 为 set 方法入口参数。
由源代码分析可知,如果一个线程不调用 ThreadLocal 实例的 set()
方法,那么 Thread 的 threadLocals 内部实例永远得不到初始化。这就是线程的上下文延迟加载,延迟,指的是相对于线程实例初始化延迟。
4. 单线程绑定多 ThreadLocal 实例
第二节实际上已经体现了单线程能够绑定多个 ThreadLocal 实例,我们为 main 线程绑定了 3 个 ThreadLocal 实例,并且 main 线程向它们存储的上下文数据是互不影响的。
ThreadLocal 实例是如何做到与多个 Map 绑定?
这是因为 Thread 类内部的 ThreadLocal.ThreadLocalMap
是一个定制版本的 HashMap,其 key 为 ThreadLocal,value 为线程为其放置的数据。
所以在第二节中的 main 线程中的 ThreadLocalMap 可以用下表表示:
Main 线程内部 ThreadLocal.ThreadLocalMap
实例的内部结构:
Key | Value |
---|---|
threadLocal1 | “hello world” |
threadLocal2 | HashMap<String, Long> |
contextThreadLocal | threadContext |
null | null |
… | … |
注意,当 ThreadLocalMap 初始化时的大小是 16。
所以当 Main 线程调用 threadLocal1.get()
方法时,实际上是再访问自己内部的 ThreadLocal.ThreadLocalMap
实例,访问的 key 为 threadLocal1
,返回的结果是 “hello world”。
为什么会出现一个线程绑定多个 ThreadLocal 实例的情况?
这是因为 ThreadLocal 是一个泛型类,定义为:public class ThreadLocal<T> {}
,如果线程想要存储多种不同类型的数据于上下文,那么最简单的方式就是给不同泛型类型 ThradLocal 存储数据。
5. 单 ThreadLocal 实例被多个线程共享
线程向一个 ThreadLocal 实例取数据的内部过程如下图所示:
线程对象借助于 ThreadLocal 来存储特定上下文数据,但是上下文数据位于线程而不位于 ThreadLocal 上,因此 ThreadLocal 进行上下文数据存储是线程安全的,这部分数据实际上是单线程独占的。
某个具体的 ThreadLocal<T>
限制了所有线程向其存取数据的类型只能为 T 类型,如果要存取其他类型,线程只好找另一个类型合适的 ThreadLocal 实例。
有上述图可见,ThreadLocal 是一个彻底的工具类,本身不存储任何上下文信息,不同 ThreadLocal 实例之间仅仅是泛型 T 不同,执行的逻辑是完全相同的。
6. 线程上下文的内存回收问题
一旦线程运行结束,与其配套的线程上下文也应当被垃圾回收。
虽然线程存储上下文数据时借助于多个 ThreadLocal,ThreadLocal 实例又可能被多个 Thread 共享,但是上下文数据始终存储于 Thread 实例中,所以上下文数据是否被回收取决于 Thread 实例,而不是 ThreadLocal 实例。
当线程运行结束后,JVM 会调用 Thread 的 exit()
方法:
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
这里将 threadLocals 变量赋值为 null,目的是方便垃圾回收器回收上下文数据所占据的内存空间。
7. InheritableThreadLocal 类
线程上下文可以利用 ThreadLocal 类实现。而线程中有一个概念:父线程和子线程。父线程负责创建子线程,并且我们希望父线程能够利用子线程的上下文,所以提供了 InheritableThreadLocal 类。
InheritableThreadLocal 的源码如下:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
其继承于 ThreadLocal()
实例,其仅仅重写了 ThreadLocal 类的 3 个方法。
-
childValue()
方法:此方法在 ThreadLocal 实例中调用直接会抛出异常。而在 InheritableThreadLocal 类中代表将父类的 value 值转换为子类的 value,默认实现是不进行转换。如果需要转换则应当重写此方法。 -
getMap()
方法:重写的目的在于返回线程的 inheritableThreadLocals 实例,原本是返回线程的 ThreadLocal 实例。 -
creteMap()
方法:重写的目的在于原本方法会将类型为 ThreadLocal 的 this 作为键值,重写后将类型为 InheritableThreadLocal 的 this 作为键值。
后面两个方法和 ThreadLocal 的设计没有任何区别,InheritableThreadLocal 的特点在于其并非是延迟加载的。
当利用 new 关键字构造一个 Thread 实例时,总是会调用 init()
,方法声明如下:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {//...}
其中,参数 boolean inheritThreadLocals
如果为 true,那么就会使正在构造的线程的 inheritableThreadLocals 实例得到父线程的 inheritableThreadLocals,在默认情况下此值就是为 true。所以简单调用 new Thread()
,构造的线程实例会拥有当前线程的上下文数据的引用。
下面看看 init()
方法是如何将父线程的 ThreadLocal 数据引用给予子线程的 InheritableThreadLocal 的:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
// 省略无关代码
...
Thread parent = currentThread();
...
// 省略无关代码
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
然后其内部方法栈最终会调用如下的构造方法:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
此构造方法最终完成了将父线程的 inheritableThreadLocals 赋值给子线程 inheritableThreadLocals。注意在子线程的 inheritableThreadLocals 的 Key 仍然为父线程 inheritableThreadLocals 中的键值,但是 value 因为调用了 childValue()
方法可能会进行转变。
综上所述,InheritableThreadLocals 和 ThreadLocal 最大的不同在于前者有父子线程的继承性,且赋值过程不是延迟加载,而是构造时就加载。
整个过程如下面两个表所示:
父线程有如下所示的 InheritableThreadLocals 内部实例(注意其类型,不为 ThreadLocal)
Key | Value |
---|---|
threadLocal1 | “hello world” |
threadLocal2 | HashMap<String, Long> |
contextThreadLocal | threadContext |
null | null |
… | … |
且父线程在创建子线程时,inheritThreadLocals
参数为 true,那么此时子线程的内部实例 InheritableThreadLocals 数据如下表所示:
Key | Value |
---|---|
threadLocal1 | childValue(“hello world”) |
threadLocal2 | childValue(HashMap<String, Long>) |
contextThreadLocal | childValue(threadContext) |
null | null |
… | … |
所以父线程在设计其上下文时,如果打算将某些上下文数据对子线程可见(具有继承性可以继续传给下一个子线程),那么应当将这部分数据放到 inheritThreadLocals 实例中去。如果部分子线程觉得自己完全没有必要得到父线程的上下文,那么在构造时就将 init()
方法的入口参数 inheritThreadLocals
参数设置为 false。
父线程可以决定将哪些上下文用于分享给子线程,子线程在构造时通过修改入口参数,也有充分地*度拒绝父线程的上下文信息。
上一篇: antd 常用知识点和小技巧总结