@[toc]
一、引入
在多线程环境中,我们都知道多个线程对共享变量进行修改操作时,容易发生线程不安全的问题,如下代码所示
public class ThreadLocalTest {
// 共享变量
private static Integer num = 1;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
if (num == 1) {
// 修改操作
num++;
}
});
Thread thread2 = new Thread(() -> {
if (num == 1) {
// 修改操作
num++;
}
});
thread1.start();
thread2.start();
}
}
当线程1thread1
在执行num++
时被阻塞,此时由于num=1
,线程2thread2
中num==1
成立,并执行num++
操作,此时num=2
,紧接着线程1thread1
得到运行时间片并执行num++
,得到的结果是num=3
,这个结果与其预期结果是不一致的。
那怎么办呢?我们索性不用共享变量了,把该变量声明在每一个线程内部作为局部变量,如下代码所示
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
// 将共享变量声明到线程内部
Integer num = 1;
if (num == 1) {
num++;
}
});
Thread thread2 = new Thread(() -> {
// 将共享变量声明到线程内部
Integer num = 1;
if (num == 1) {
num++;
}
});
thread1.start();
thread2.start();
}
}
此时运行代码,两个线程的运行结果均可以符合预期,但是我们就无法使用共享变量了,这将极大增加我们代码的复杂度,比如我们有100个线程,如果在每个线程内部都设置一个相同的局部变量,那就太low了。
那么如何既使用共享变量,又能避免线程不安全的问题呢?
下面引出本文主角:ThreadLocal
。
二、介绍
先看一下ThreadLocal
的官方解释。
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
翻译成人话就是:我们可以通过ThreadLocal
实例隐式地给线程设置局部变量,即该实例可以在每一个线程内部创建一个变量的副本。而该ThreadLocal
实例我们就可以通过全局共享变量的方式创建。如下图所示
很多文章中将ThreadLocal
解释为多线程环境中保证线程安全的一个类,但这种说法其实是十分片面的。
- 从官方解释上来看,
ThreadLocal
的功能仅仅是为了给多个线程设置局部变量,这和线程安全是没有关系的,线程安全针对的是全局的共享变量。 从类路径来看,
ThreadLocal
位于java.lang
包,而非java.util.concurrent
包。从其使用方法来看,无论是
set()
方法、get()
方法、remove()
方法,都与线程同步或加锁没有任何关系。
那么为什么还要说他是线程安全的呢?还是因为它为每一个线程设置一个变量副本,每个线程访问或修改的都是其副本,因此可以认为它提供了一种线程安全的实现。
三、实例化
ThreadLocal
提供了两种实例化的方式,第一种是使用静态方法withInitial
实例化,另一种是使用new
实例化。
1. 使用静态方法withInitial
实例化
ThreadLocal
提供了一个静态方法withInitial(Supplier<? extends S> supplier)
实例化ThreadLocal
对象,如下所示
private ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 1);
我们看一下withInitial()
方法的源码如下
public class ThreadLocal<T> {
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
}
从源码中可知,我们实例化的ThreadLocal
对象的实际类型为SuppliedThreadLocal
,它继承于ThreadLocal
,其内部十分简单
以函数式接口
Supplier
为参数的构造方法接收
Supplier
对象为参数,并赋值给supplier
属性,可理解为一个工厂方法,与spring中的FactoryBean
作用相同。重写
ThreadLocal
类的initalValue()
方法该方法用于从
supplier
工厂中获取实例对象。
2. 使用new
实例化
new
的方式十分简单
private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
以上两种实例化ThreadLocal
对象方式的不同之处为:
使用静态方法withInitial()
实例化的ThreadLocal
对象中存在一个对象工厂supplier
,因此当我们首次调用get()
方法时,ThreadLocal
内部直接从该对象工厂supplier
中获取对象作为当前线程中的变量副本。
使用new
实例化的ThreadLocal
对象中不存在对象工厂supplier
,因此我们在首次调用get()
方法之前,需要调用set()
方法对当前线程设置变量值。
四、Thread
与静态内部类ThreadLocalMap
ThreadLocal
中有一个十分重要的静态内部类ThreadLocalMap
,它实质上是一个以<key, value>
为键值对的entry数组,其中key为当前ThreadLocal
实例(即this
),value为前面从Supplier
工厂中获取的值或通过set()
方法保存的值。如下图所示
ThreadLocalMap
结构很简单,但它有一个不太容易理解的地方是对它的引用。
我们把目光转移到Thread
类中,在Thread
类中有一个ThreadLocalMap
类型的成员属性threadLocals
,源码如下所示
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
关联上面ThreadLocalMap
结构图,我们可以对ThreadLocal
、ThreadLocalMap
、Thread
三者之间的关系做出如下阐述:
每一个线程Thread
中维护着一个ThreadLocalMap
实例,在ThreadLocalMap
实例中保存着一个map集合,该map集合以ThreadLocal
实例为key,以ThreadLocal
实例中Supplier
工厂中提供的值或通过set()
方法保存的值为value。
简言之,每一个线程Thread
中维护着一组以ThreadLocal
实例为key的map集合。
我们使用下面代码结合图片的方式对其进行理解。
public class ThreadLocalTest {
// 使用withInitail()静态方法实例化ThreadLocal对象
private static ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 1);
// 使用new的方式实例化ThreadLocal对象
private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// 设置当前线程局部变量thread1
stringThreadLocal.set("threa1");
// 获取当前线程局部变量thread1
String s = stringThreadLocal.get();
System.out.println("线程1中stringThreadLocal的值:" + s);
// 获取当前线程局部变量integer
Integer integer = integerThreadLocal.get();
System.out.println("线程1中integerThreadLocal的值:" + integer);
});
Thread thread2 = new Thread(() -> {
// 设置当前线程局部变量thread2
stringThreadLocal.set("threa2");
// 获取当前线程局部变量thread1
String s = stringThreadLocal.get();
System.out.println("线程2中stringThreadLocal的值:" + s);
// 获取当前线程局部变量integer
Integer integer = integerThreadLocal.get();
System.out.println("线程2中integerThreadLocal的值:" + integer);
});
thread1.start();
thread2.start();
}
}
输出如下:
线程1中stringThreadLocal的值:threa1
线程2中stringThreadLocal的值:threa2
线程1中integerThreadLocal的值:1
线程2中integerThreadLocal的值:1
在上面的代码中,线程1thread1
和线程2thread2
使用两个共享变量integerThreadLocal
和stringThreadLocal
,其中integerThreadLocal
使用静态方法withInitial()
定义了一个Supplier
对象工厂,线程1和线程2调用get()
方法时从该对象工厂中获取值,另外,在线程1thread1
和线程2thread2
中,分别使用set()
方法对stringThreadLocal
设置字符串值thread1
和thread2
。
ThreadLocal
、ThreadLocalMap
、Thread
三者之间的关系通过图示的方式将其表现出来如下所示
五、initialValue()方法
ThreadLocal中该方法源码如下:
protected T initialValue() {
return null;
}
该方法用于返回当前线程的这个线程局部变量的“初始值”。该方法将在线程第一次使用get()
方法访问变量时调用,除非该线程先前调用了set()
方法,在这种情况下,将不会为该线程调用initialValue()
方法。通常,每个线程最多调用此方法一次,但在随后调用remove()
和get()
的情况下,可能会再次调用该方法。
这个实现简单地返回null
;如果程序员希望线程局部变量具有非null的初始值,则必须将ThreadLocal子类化,并重写此方法。通常,将使用匿名内部类。
刚巧不巧,ThreadLocal内部实现了其子类SuppliedThreadLocal
,并对initialValue()
方法进行了重写。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
重写后的initialValue()
方法返回的是从Supllier
工厂中获取的实例。
六、set()方法
set()
方法用于设置当前线程ThreadLocalMap
属性中的当前ThreadLocal
实例(即this
)对应的值。源码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
源码也是非常简单的,无非就是获取当前线程的ThreadLocalMap
属性,然后将当前ThreadLocal实例(即this
)作为key保存到ThreadLocalMap
属性中。
七、get()方法
get()
方法用于返回当前线程ThreadLocalMap
属性中的当前ThreadLocal
实例(即this
)对应的值。如果变量对当前线程没有值,则通过setInitialValue()
方法从initialValue()
方法中获取值,将获取到的值保存到当前线程ThreadLocalMap
属性中后再返回该值。
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();
}
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;
}
在setInitialValue()
方法中,我们看到先通过initialValue()
方法从Supplier
工厂中获取到值,剩下的逻辑和set()
方法相同,最后将该值返回。
八、remove()方法
该方法逻辑就是将当前线程ThreadLocalMap
属性中的以当前ThreadLocal
实例(即this
)为key的键值对移除。源码如下
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
九、本地变量的继承性
现在我们探讨一个问题,ThreadLocal在父线程中创建的局部变量副本能被其子线程获取吗?如果你对上面ThreadLocal
、ThreadLocalMap
、Thread
三者之间的关系有很好的理解的话,我们可以换个方式提问:子线程中的ThreadLocalMap
集合能够继承父线程中的ThreadLocalMap
集合吗?
我们使用以下代码示例说明
public class ThreadLocalTest {
// 使用withInitail()静态方法实例化ThreadLocal对象
private static ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 1);
// 使用new的方式实例化ThreadLocal对象
private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// 设置当前线程局部变量thread1
stringThreadLocal.set("threa1");
// 获取当前线程局部变量thread1
String s = stringThreadLocal.get();
System.out.println("线程1中stringThreadLocal的值:" + s);
// 获取当前线程局部变量integer
Integer integer = integerThreadLocal.get();
System.out.println("线程1中integerThreadLocal的值:" + integer);
new Thread(() -> {
// 获取从父线程中继承的string变量
System.out.println("线程1_1中stringThreadLocal的值:" + stringThreadLocal.get());
// 获取从父线程中继承的integer变量
System.out.println("线程1_1中integerThreadLocal的值:" + integerThreadLocal.get());
}).start();
});
Thread thread2 = new Thread(() -> {
// 设置当前线程局部变量thread2
stringThreadLocal.set("threa2");
// 获取当前线程局部变量thread1
String s = stringThreadLocal.get();
System.out.println("线程2中stringThreadLocal的值:" + s);
// 获取当前线程局部变量integer
Integer integer = integerThreadLocal.get();
System.out.println("线程2中integerThreadLocal的值:" + integer);
new Thread(() -> {
// 获取从父线程中继承的string变量
System.out.println("线程2_1中stringThreadLocal的值:" + stringThreadLocal.get());
// 获取从父线程中继承的integer变量
System.out.println("线程2_1中integerThreadLocal的值:" + integerThreadLocal.get());
}).start();
});
thread1.start();
thread2.start();
}
}
在该示例代码中,我们通过withInitial()
和new
两种方式创建两个全局ThreadLocal实例:integerThreadLocal
和 stringThreadLocal
,
integerThreadLocal
通过Supplier
对象工厂自动在当前线程中创建局部变量副本stringThreadLocal
通过我们在线程中手动调用set()
方法创建局部变量副本
线程1thread1
和 线程2thread2
中又分别创建其子线程,并在子线程中获取integerThreadLocal
和 stringThreadLocal
创建的局部变量副本。
运行代码后输出如下:
线程1中stringThreadLocal的值:threa1
线程2中stringThreadLocal的值:threa2
线程1中integerThreadLocal的值:1
线程2中integerThreadLocal的值:1
线程1_1中stringThreadLocal的值:null
线程1_1中integerThreadLocal的值:1
线程2_1中stringThreadLocal的值:null
线程2_1中integerThreadLocal的值:1
从输出中我们看到,如果ThreadLocal实例是通过withInitial()
方法创建的,那么无论是子线程还是孙线程,我们都是可以获取到局部变量副本的。但是如果ThreadLocal实例是通过new
的方法创建的,由于在子线程中没有调用set()
方法设置变量副本,所以也就无法获取到当前子线程中的局部变量副本。
那么如何使局部变量副本在父子关系的线程中具有继承性呢?请看lnheritableThreadLocal
。
1. lnheritableThreadLocal可继承的ThreadLocal
lnheritableThreadLocal
是ThreadLocal
的子类,从其命名可以看出,它是个具有继承功能的ThreadLocal
,官方解释如下:
这个类扩展了ThreadLocal,以提供从父线程到子线程的值继承:当创建子线程时,子线程接收父线程有值的所有可继承的线程局部变量的初始值。
我们看一下它的源码,没几行
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);
}
}
这么短短的几行代码就能实现线程局部变量的继承,关键之处在t.inheritableThreadLocals
,无论是获取还是实例化ThreadLocalMap
,都是和线程Thread
的inheritableThreadLocals
相关。
其实,Thread
类中存在两个ThreadLocalMap
类型的属性:threadLocals
和 inheritableThreadLocals
。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
- 如果当前
ThreadLocal
实例的实际类型为ThreadLocal
,则线程局部变量副本保存在threadLocals
中 - 如果当前
ThreadLocal
实例的实际类型为InheritableThreadLocal
,则线程局部变量副本保存在inheritableThreadLocals
中
我们通过以下代码进行演示
public class ThreadLocalTest {
// 使用new的方式实例化ThreadLocal对象
private static ThreadLocal<String> stringInheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// 设置当前线程局部变量thread1
stringInheritableThreadLocal.set("threa1");
// 获取当前线程局部变量thread1
String s = stringInheritableThreadLocal.get();
System.out.println("线程1中stringThreadLocal的值:" + s);
new Thread(() -> {
// 获取从父线程中继承的string变量
System.out.println("线程1_1中stringThreadLocal的值:" + stringInheritableThreadLocal.get());
}).start();
});
Thread thread2 = new Thread(() -> {
// 设置当前线程局部变量thread2
stringInheritableThreadLocal.set("threa2");
// 获取当前线程局部变量thread1
String s = stringInheritableThreadLocal.get();
System.out.println("线程2中stringThreadLocal的值:" + s);
new Thread(() -> {
// 获取从父线程中继承的string变量
System.out.println("线程2_1中stringThreadLocal的值:" + stringInheritableThreadLocal.get());
}).start();
});
thread1.start();
thread2.start();
}
}
运行代码后输出如下:
线程1中stringThreadLocal的值:threa1
线程2中stringThreadLocal的值:threa2
线程1_1中stringThreadLocal的值:threa1
线程2_1中stringThreadLocal的值:threa2
由此可见,使用InheritableThreadLocal
时,在父线程中通过set()
方法设置的局部变量副本,可以继承到其子线程的局部变量副本中。
纸上得来终觉浅,绝知此事要躬行。
————————我是万万岁,我们下期再见————————