面试官:解释一下 ThreadLocal 核心原理

在并发编程中,除了可以使用锁机制来保证线程安全,JDK 中还提供了 ThreadLocal 类来保证多个线程能够安全访问共享变量。本章简单介绍 ThreadLocal 的核心原理。

本文涉及的知识点如下。

—ThreadLocal 的基本概念。

—ThreadLocal 的使用案例。

—ThreadLocal 的核心原理。

—ThreadLocal 变量的继承性。

—InheritableThreadLocal 的使用案例。

—InheritableThreadLocal 的核心原理。

ThreadLocal 的基本概念

在并发编程中,多个线程同时访问同一个共享变量,可能会出现线程安全的问题。为了保证在多线程环境下访问共享变量的安全性,通常会在访问共享变量的时候加锁,以实现线程同步的效果。

使用同步锁机制保证多线程访问共享变量的安全性的原理如图 14-1 所示。该机制能够保证同一时刻只有一个线程访问共享变量,从而确保在多线程环境下访问共享变量的安全性。

另外,为了更加灵活地确保线程的安全性,JDK 中提供了一个 ThreadLocal 类,ThreadLocal 类能够支持本地变量。在使用 ThreadLocal 类访问共享变量时,会在每个线程的本地内存中都存储一份这个共享变量的副本。在多个线程同时对这个共享变量进行读写操作时,实际上操作的是本地内存中的变量副本,多个线程之间互不干扰,从而避免了线程安全的问题。使用 ThreadLocal 访问共享变量的示意图如图 14-2 所示。

ThreadLocal 的使用案例

本节主要实现两个通过 ThreadLocal 操作线程本地变量的案例,以此加深读者对 ThreadLocal 的理解。

案例一的主要实现逻辑:在案例程序中分别创建名称为 Thread-A 和 Thread-B 的两个线程, 在 Thread-A 线程和 Thread-B 线程的 run()方法中通过 ThreadLocal 保存本地变量,随后打印 Thread-A 线程和 Thread-B 线程中保存的本地变量。最后,启动 Thread-A 线程和 Thread-B 线程。

案例一的核心代码如下。

/**
*@author binghe
*@version 1.0.0
*@description ThreadLocal 案例程序
*/
public class ThreadLocalTest {

private static final ThreadLocal<String> THREAD_LOCAL = new 
ThreadLocal<String>();

public static void main(String[] 
args){ Thread threadA = new 
Thread(()->{
THREAD_LOCAL.set("ThreadA: " + Thread.currentThread().getName());
 System.out.println(Thread.currentThread().getName() + "本地变量中的值为: "
+ THREAD_LOCAL.get());
}, "Thread-A");

Thread threadB = new Thread(()->{
THREAD_LOCAL.set("ThreadB: " + Thread.currentThread().getName());
 System.out.println(Thread.currentThread().getName() + "本地变量中的值为: "
+ THREAD_LOCAL.get());
}, "Thread-B");

threadA.start();
 threadB.start();
}
}

运行上述代码,输出结果如下。

Thread-A 本地变量中的值为: ThreadA: Thread-A

Thread-B 本地变量中的值为: ThreadB: Thread-B

从输出结果可以看出,Thread-A 线程和 Thread-B 线程通过 ThreadLocal 保存了本地变量, 并未正确打印出结果。

案例二的主要实现逻辑:在案例一的基础上为 Thread-B 线程增加删除 ThreadLocal 中保存的本地变量的操作,随后打印结果来证明删除 Thread-B 线程中的本地变量不会影响 Thread-A 线程中的本地变量。

案例二的核心代码如下。

/**
*@author binghe
*@version 1.0.0
* @description ThreadLocal 案例程序
*/
public class ThreadLocalTest {
private static final ThreadLocal<String> THREAD_LOCAL = new 
ThreadLocal<String>();

public static void main(String[] 
args){ Thread threadA = new 
Thread(()->{
THREAD_LOCAL.set("ThreadA: " + Thread.currentThread().getName()); 
System.out.println(Thread.currentThread().getName() + "本地变量中的值为: "
+ THREAD_LOCAL.get());
System.out.println(Thread.currentThread().getName() + "未删除本地变量,本地变量中的值为: " + THREAD_LOCAL.get());
}, "Thread-A");

Thread threadB = new Thread(()->{
THREAD_LOCAL.set("ThreadB: " + Thread.currentThread().getName()); 
System.out.println(Thread.currentThread().getName() + "本地变量中的值为: "
+ THREAD_LOCAL.get());
THREAD_LOCAL.remove();
System.out.println(Thread.currentThread().getName() + "删除本地变量后,本地变量中的值为: " + THREAD_LOCAL.get());
}, "Thread-B");

threadA.start(); 
threadB.start();

运行上述代码,输出结果如下。

Thread-A 本地变量中的值为: ThreadA: Thread-A

Thread-A 未删除本地变量,本地变量中的值为: ThreadA: Thread-A

Thread-B 本地变量中的值为: ThreadB: Thread-B

Thread-B 删除本地变量后,本地变量中的值为: null

结论:Thread-A 线程和 Thread-B 线程存储在 ThreadLocal 中的变量互不干扰,Thread-A 线程中存储的本地变量只能由 Thread-A 线程访问,Thread-B 线程中存储的本地变量只能由 Thread-B 线程访问。

从输出结果可以看出,删除 Thread-B 线程中的本地变量后,Thread-B 线程中保存的本地变量的值为 null。同时,删除 Thread-B 线程中的本地变量后,不会影响 Thread-A 线程中保存的本地变量。

ThreadLocal 的核心原理

ThreadLocal 能够保证每个线程操作的都是本地内存中的变量副本。在底层实现上,调用 ThreadLocal 的 set()方法会将本地变量保存在具体线程的内存空间中,而 ThreadLocal 并不负责存储具体的数据。

Thread 类源码

在 Thread 类的源码中,定义了两个 ThreadLocal.ThreadLocalMap 类型的成员变量,分别为 threadLocals 和 inheritableThreadLocals,源码如下。

public class Thread implements Runnable {
/*********** 省 略 N 行 代 码 *************/ 
ThreadLocal.ThreadLocalMap threadLocals = null; 
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
/***********省略 N 行代码*************/
}

在 Thread 类中定义成员变量 threadLocals 和 inheritableThreadLocals,二者的初始值都为 null,并且只有当线程第一次调用 ThreadLocal 或者 InheritableThreadLocal 的 set()方法或者 get()方法时才会实例化变量。

上述代码也说明,通过 ThreadLocal 为每个线程保存的本地变量不是存储在 ThreadLocal 实例中的,而是存储在调用线程的 threadLocals 变量中的。也就是说,调用 ThreadLocal 的 set() 方法存储的本地变量在具体线程的内存空间中,而 ThreadLocal 类只是提供了 set()和 get()方法来存储和读取本地变量的值,当调用 ThreadLocal 类的 set()方法时,把要存储的值存储在调用线程的 threadLocals 变量中,当调用 ThreadLocal 类的 get()方法时,从当前线程的 threadLocals 变量中获取保存的值。

set()方法

ThreadLocal 类中 set()方法的源码如下。

public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程为 key,获取 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
//获取的ThreadLocalMap 对象不为空
if (map != null)
//设置 value 的值
map.set(this, value); else
//获取的 ThreadLocalMap 对象为空,实例化 Thread 类中的 threadLocals 变量
createMap(t, value);
}

从 ThreadLocal 类中的 set()方法的源码可以看出,在 set()方法中,会先获取调用 set()方法的线程,然后使用当前线程对象作为 key 调用 getMap(t)方法获取 ThreadLocalMap 对象,其中, getMap(Thread t)方法的源码如下。

ThreadLocalMap getMap(Thread t) 
{ return t.threadLocals;
}

通过 getMap(Thread t)方法的源码可以看出,调用 getMap(Thread t)方法获取的就是当前线程中定义的 threadLocals 成员变量。

再次回到 ThreadLocal 的 set() 方法中,调用 getMap(Thread t) 方法并将结果赋值给 ThreadLocalMap 类型的变量 map,判断 map 的值是否为空,也就是判断调用 getMap(Thread t) 方法返回的当前线程的 threadLocals 成员变量是否为空。

如果当前线程的 threadLocals 成员变量不为空,则把 value 设置到 Thread 类的 threadLocals 成员变量中。此时,保存数据时传递的 key 为当前 ThreadLocal 的 this 对象,而传递的 value 为调用 set()方法传递的值。

如果当前线程的 threadLocals 成员变量为空,则调用 createMap(t, value)方法来实例化当前线程的 threadLocals 成员变量,并保存 value 值。createMap(t, value)源码如下。

void createMap(Thread t, T firstvalue) {
t.threadLocals = new ThreadLocalMap(this, firstvalue);
}

至此,ThreadLocal 类中的 set()方法分析完毕。

get()方法

ThreadLocal 类中 get()方法的源码如下。

public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的 threadLocals 成员变量
ThreadLocalMap map = getMap(t);
//获取的threadLocals 成员变量不为空
if (map != null) {
//返回本地变量对应的值
ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) {
@SuppressWarnings("unchecked") T result = (T)e.value;
return result;
}
}
//初始化threadLocals 成员变量的值
return setInitialvalue();
}

通过 ThreadLocal 类中 get()方法的源码可以看出,get()方法会通过调用 getMap(Thread t)方法并传入当前线程来获取 threadLocals 成员变量,随后判断当前线程的 threadLocals 成员变量是否为空。

如果 threadLocals 成员变量不为空,则直接返回当前线程 threadLocals 成员变量中存储的本地变量的值。

如果 threadLocals 成员变量为空,则调用 setInitialvalue()方法来初始化 threadLocals 成员变量的值。

setInitialvalue()方法的源码如下。

private T setInitialvalue() {
//调用初始化 value 的方法
T value = initialvalue();
Thread t = Thread.currentThread();
//以当前线程为 key 获取 threadLocals 成员变量
ThreadLocalMap map = getMap(t); if (map != null)
//threadLocals 不为空,则设置value 值
map.set(this, value); else
//threadLocals 为空,则实例化 threadLocals 成员变量
createMap(t, value); return value;
}

setInitialvalue()方法与 set()方法的主体逻辑大致相同,只不过 setInitialvalue()方法会先调用 initialvalue()方法来初始化 value 的值,同时,在方法的最后会返回 value 的值。

initialvalue()方法的源码如下。

protected T initialvalue() 
{ return null;
}

可以看到,ThreadLocal 类的 initialvalue()方法会直接返回 null,方法的具体逻辑会交由 ThreadLocal 类的子类实现。

至此,ThreadLocal 类中的 get()方法分析完毕。

remove()方法

ThreadLocal 类中 remove()方法的源码如下。

public void remove() {
//调用 getMap()方法并传入当前线程对象获取 threadLocals 成员变量
ThreadLocalMap m = getMap(Thread.currentThread()); 
if (m != null)
//threadLocals 成员变量不为空,则移除 value 值
m.remove(this);
}

remove()方法的实现比较简单,根据调用的 getMap()方法获取当前线程的 threadLocals 成员变量,如果当前线程的 threadLocals 成员变量不为空,则直接从当前线程的 threadLocals 成员变量中移除当前 ThreadLocal 对象对应的 value 值。

注意:如果调用线程一直不退出,本地变量就会一直存储在调用线程的 threadLocals 成员变量中,所以,如果不再需要使用本地变量,那么可以通过调用 ThreadLocal 的 remove()

方法,将本地变量从当前线程的 threadLocals 成员变量中删除,以避免出现内存溢出的问题。

至此,ThreadLocal 类中的 remove()方法分析完毕。

ThreadLocal 变量的不继承性

在使用 ThreadLocal 存储本地变量时,主线程与子线程之间不具有继承性。在主线程中使用 ThreadLocal 对象保存本地变量后,无法通过同一个 ThreadLocal 对象获取到在主线程中保存的值。

例如,下面的代码在主线程中使用 ThreadLocal 对象保存了本地变量的值,但是在子线程中使用同一个 ThreadLocal 对象获取到的值为空。

/**
*@author binghe
*@version 1.0.0
*@description 测试 ThreadLocal 的继承性
*/
public class ThreadLocalInheritTest {

private static final ThreadLocal<String> THREAD_LOCAL = new 
ThreadLocal<String>();

public static void main(String[] args){
//在主线程中通过 THREAD_LOCAL 保存值
THREAD_LOCAL.set("binghe");

//在子线程中通过 THREAD_LOCAL 获取在主线程中保存的值
new Thread(()->{
System.out.println("在子线程中获取到的本地变量的值为: " + 
THREAD_LOCAL.get());
} ).start();

//在主线程中通过 THREAD_LOCAL 获取在主线程中保存的值
System.out.println("在主线程中获取到的本地变量的值为: " + THREAD_LOCAL.get());
}
}

首先,在 ThreadLocalInheritTest 类中定义了一个 ThreadLocal 类型的常量 THREAD_LOCAL。然后在 main()方法中,使用 THREAD_LOCAL 保存了一个字符串类型的本地变量,值为 binghe。接下来,在子线程中打印通过 THREAD_LOCAL 获取到的本地变量的值,最后,在 main()方法中通过 THREAD_LOCAL 获取本地变量的值。

运行 ThreadLocalInheritTest 类的代码,输出的结果如下。

在主线程中获取到的本地变量的值为: binghe

在子线程中获取到的本地变量的值为: null

通过输出结果可以看出,在主线程中通过 ThreadLocal 对象保存值后,在子线程中通过相同的 ThreadLocal 对象是无法获取到这个值的。

如果需要在子线程中获取到在主线程中保存的值,则可以使用 InheritableThreadLocal 对象。

InheritableThreadLocal 的使用案例

InheritableThreadLocal 类在结构上继承自 ThreadLocal 类。所以,InheritableThreadLocal 类的使用方式与 ThreadLocal 相同。这里,可以将 14.4 节代码中的 ThreadLocal 修改为 InheritableThreadLocal,修改后的代码如下。

/**
*@author binghe
*@version 1.0.0
*@description 测试 InheritableThreadLocal 的继承性
*/
public class InheritableThreadLocalTest {

//将创建的 ThreadLocal 对象修改为 InheritableThreadLocal 对象private static final ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<String>();

public static void main(String[] args){
//在主线程中通过 THREAD_LOCAL 保存值
THREAD_LOCAL.set("binghe");

//在子线程中通过 THREAD_LOCAL 获取在主线程中保存的值
new Thread(()->{
System.out.println("在子线程中获取到的本地变量的值为: " + THREAD_LOCAL.get());
} ).start();

//在主线程中通过 THREAD_LOCAL 获取在主线程中保存的值
System.out.println("在主线程中获取到的本地变量的值为: " + THREAD_LOCAL.get());
}
}

可以看到,这里在 14.4 节的案例基础上,仅仅修改了 THREAD_LOCAL 常量实例化后的对象类型,由原来的 ThreadLocal 类型修改为 InheritableThreadLocal 类型。

运行 InheritableThreadLocalTest 类的源码,输出结果如下。

在主线程中获取到的本地变量的值为: binghe

通过输出结果可以看出,在主线程中通过 InheritableThreadLocal 对象保存值后,在子线程中通过相同的 InheritableThreadLocal 对象可以获取到这个值。说明 InheritableThreadLocal 类的对象保存的变量具有继承性。

InheritableThreadLocal 的核心原理

InheritableThreadLocal 类继承自 ThreadLocal 类,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);
}

在 InheritableThreadLocal 类中重写了 ThreadLocal 类中的 childvalue()方法、getMap()方法和 createMap()方法。使用 InheritableThreadLocal 保存变量,当调用 ThreadLocal 的 set()方法时,创建的是当前线程的 inheritableThreadLocals 成员变量,而不是当前线程的 threadLocals 成员变量。

通过分析 Thread 类的源码可知,InheritableThreadLocal 类的 childvalue()方法是在 Thread 类的构造方法中调用的。通过查看 Thread 类的源码可知,Thread 类的构造方法如下。

public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

Thread(Runnable target, AccessControlContext acc) {
init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}

public Thread(ThreadGroup group, Runnable target) { init(group, target, "Thread-" + nextThreadNum(), 0);
}
init(null, null, name, 0);
}

public Thread(ThreadGroup group, String name) { init(group, null, name, 0);
}

public Thread(Runnable target, String name) { init(null, target, name, 0);
}

public Thread(ThreadGroup group, Runnable target, String name) { init(group, target, name, 0);
}

public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
init(group, target, name, stackSize);
}

可以看到,在 Thread 类的每个构造方法中,都会调用 init()方法,init()方法的部分代码如下。

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
/************省略部分源码************/
if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

this.stackSize = stackSize; tid = nextThreadID();
}

可以看到,在 init()方法中,会判断传递的 inheritThreadLocals 变量是否为 true,同时会判断父线程中的 inheritableThreadLocals 成员变量是否为 null。如果传递的 inheritThreadLocals 变量为 true,同时父线程中的 inheritableThreadLocals 成员变量不为 null,则调用 ThreadLocal 类的 createInheritedMap()方法来创建 ThreadLocalMap 对象。

ThreadLocal 类的 createInheritedMap()方法的源码如下。

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) 
{ return new ThreadLocalMap(parentMap);
}

在 ThreadLocal 类的 createInheritedMap()方法中,会使用父线程的 inheritableThreadLocals 成员变量作为入参调用 ThreadLocalMap 类的构造方法来创建新的 ThreadLocalMap 对象。并在 Thread 类的 init() 方 法中将创 建的 ThreadLocalMap 对象赋值 给当前线程 的 inheritableThreadLocals 成员变量,也就是赋值给子线程的 inheritableThreadLocals 成员变量。

接下来,分析 ThreadLocalMap 类的构造方法,源码如下。

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++;
}
}
}
}

ThreadLocalMap 类的构造方法是私有的,在 ThreadLocalMap 类的构造方法中,有如下一行代码。

Object value = key.childvalue(e.value);

这行代码调用了 InheritableThreadLocal 类重写的 childvalue() 方法, 也就是说, 在 ThreadLocalMap 类的构造方法中调用了 InheritableThreadLocal 类重写的 childvalue()方法。另外, 在 InheritableThreadLocal 类中重写了 getMap() 方法来 确保获取 的是线程 的 inheritableThreadLocals 成员变量, 同时,重写了 createMap() 方法来确保创建的是线程的 inheritableThreadLocals 成员变量的对象。

而线程在通过 InheritableThreadLocal 类的 set()方法和 get()方法保存和获取本地变量时,会通过 InheritableThreadLocal 类重写的 createMap()方法来创建当前线程的 inheritableThreadLocals 成员变量的对象。

如果在某个线程中创建子线程,就会在 Thread 类的构造方法中把父线程的 inheritableThreadLocals 成员 变量 中保 存的 本地 变量 复制 一份 保存 到子 线程的 inheritableThreadLocals 成员变量中。

总结

本文主要对 ThreadLocal 的核心原理进行了简单的介绍。首先,介绍了 ThreadLocal 的基本概念并简单介绍了 ThreadLocal 的使用案例,然后,结合 ThreadLocal 的源码介绍了 ThreadLocal 的核心原理和 ThreadLocal 变量的不继承性。接下来,介绍了 InheritableThreadLocal 的使用案例, 并说明了 InheritableThreadLocal 变量的继承性。最后,结合 InheritableThreadLocal 和 Thread 的源码,详细分析了 InheritableThreadLocal 的核心原理。

猜你喜欢

转载自blog.csdn.net/weixin_70730532/article/details/125871469