十三、ThreadLocal

13.1、ThreadLocal 简介

1、ThreadLocal 是什么?

ThreadLocal 能提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候(通过 get 或 set 方法)都有自己独立初始化的变量副本,ThreadLocal 实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户 ID 或者事务 ID)关联起来。

总结:

  • 线程并发

ThreadLocal运用于多线程环境下

  • 传递数据

我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量

  • 线程隔离

每个线程的变量都是独立的,不会相互影响

2、ThreadLocal 能干什么?

​ ThreadLocal 能实现每一个线程都有自己专属的本地变量副本,主要解决了让每个线程绑定自己的值,通过使用 get()和 set() 方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

3、ThreadLocal 常用API介绍

​ 返回该线程局部变量在当前线程副本中的值。如果该变量对于当前线程没有值,它首先被初始化调用 initialValue 方法得到返回的值。

1
public T get() {}

​ 返回当前线程的这个线程局部变量的“初始值”。该方法将在线程第一次使用 get 方法访问变量时被调用
除非线程之前调用了set 方法,在这种情况下,initialValue 方法将不会被线程调用。
​ 通常,这个方法在每个线程中最多调用一次,但是在后续调用 remove 和 get 的情况下,它可能会被再次调用。

1
2
3
protected T initialValue() {
return null;
}

​ 删除当前线程局部变量的值。如果这个线程局部变量随后被当前线程调用了 get ,它的值将通过调用它的 initialValue 方法重新初始化,除非它的值在过渡期间被当前线程调用了 set 。这可能导致在当前线程中多次调用 initialValue 方法。

1
public void remove() {}

​ 将当前线程的这个线程局部变量的副本设置为指定的值。大多数子类将不需要覆盖这个方法,仅仅依靠 initialValue 方法来设置线程局部变量的值

1
public void set(T value) {}

​ 创建线程局部变量。变量的初始值是通过方法上 Supplier 的 get 方法来确定的。jdk1.8 才有的。

1
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}

13.2、ThreadLocal 简单使用

1、问题引出

需求:线程隔离

  • 在多线程并发场景下,每个线程中的变量都是相互独立的
  • 线程A,设置变量1,获取变量1
  • 线程B,设置变量2,获取变量2

在没有引入ThreadLocal之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class MyDemo01 {
private String content;

public static void main(String[] args) {
MyDemo01 demo = new MyDemo01();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
},String.valueOf(i)).start();
}
}
}

运行程序,发现线程间没有实现隔离,有线程拿到了自己以外的线程资源。

image-20210303151200139

2、引入ThreadLocal

set():将变量绑定到当前线程中

get():获取当前线程绑定的变量

在上面的程序中使用ThreadLocal代替content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class MyDemo01 {
private static ThreadLocal threadLocal = new ThreadLocal();

public static void main(String[] args) {
MyDemo01 demo = new MyDemo01();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
threadLocal.set(Thread.currentThread().getName() + "的数据");
System.out.println("-------------------");
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
},String.valueOf(i)).start();
}
}
}

再次运行程序,发现已经实现线程隔离

image-20210303152441357

13.3、ThreadLocal 和 synchronized的区别

synchronizedThreadLocal
原理同步机制采用以时间换空间的方式。只提供一份变量,让不同的线程排队访问。ThreadLocal采用了以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

总结:在上面的案例中,使用synchronizedThreadLocal 都可以解决问题,但使用 ThreadLocal 更为合适,因为这样可以使程序拥有更高的并发性。

13.4、ThreadLocal 的内部结构

探究实现线程数据隔离原理

1、常见的误解

​ 如果我们不去看源代码的话,可能会猜测ThreadLocal是这样子设计的:每个ThreadLocal类都创建一个Map,然后用线程作为Mapkey,要存储的局部变量作为Mapvalue,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的

image-20210303204947445

2、现在的设计

​ JDK后面优化了设计方案,在JDK8 中 ThreadLocal 的设计是:每个 Thread 都维护一个 ThreadLocalMap ,这个Map的 keyThreadLocal 实例本身,value 才是真正要存储的值 Object

​ 具体过程如下:

  • 每个Thread线程内部都有一个Map (ThreadLocalMap)
  • Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

JDK8中ThreadLocal设计图

image-20210303205447468

3、这样设计的好处

image-20210303210524365

从上图可以看出,JDK8后的设计和我们一开始猜测的设计正好相反,这样设计有如下优势:

  • 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。
  • Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

13.5、ThreadLocal的核心方法源码

除了构造方法外,ThreadLocal还有以下4个对外暴露的方法

方法描述
protected T initialValue()返回当前线程局部变量的初始值
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

1、ThreadLocalMap相关方法

ThreadLocal中有一个静态内部类ThreadLocalMap,其中ThreadLocalMap内部维护了一个Entry 数组;

除此之外,ThreadLocal中还有getMapcreateMap等一系列方法,这些方法与ThreadLocalMap息息相关。

  • ThreadLocalMap静态内部类

image-20210303212211603

  • ThreadLocalMap中的静态内部类Entry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
...
}
}
  • ThreadLocal中的getMap方法
1
2
3
4
5
6
7
8
9
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
  • ThreadLocal中的createMap方法
1
2
3
4
5
6
7
8
9
10
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • setInitialValue方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* set的变样实现,用于初始化值initialValue,
* 用于代替防止用户重写set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}

2、set方法

源码及中文注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
}

set方法代码执行流程如下

  • 首先获取当前线程,并根据当前线程获取一个Map

  • 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

  • 如果Map为空,则给该线程创建 Map,并设置初始值

3、get方法

源码及中文注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到对应的存储实体 e
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
// 调用setInitialValue进行初始化
return setInitialValue();
}

代码执行流程

  • 首先获取当前线程,根据当前线程获取一个Map

  • 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到E

  • 如果e不为null,则返回e.value,否则执行下一步操作

  • Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

get方法总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

4、remove方法

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}

代码执行流程

  • 首先获取当前线程,并根据当前线程获取一个Map

  • 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

5、initialValue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 返回当前线程对应的ThreadLocal的初始值
* 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
* 除非线程先调用了 {@link #set}方法,在这种情况下,
* {@code initialValue} 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}

此方法的作用是 返回该线程局部变量的初始值。

  • 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。

  • 这个方法缺省实现直接返回一个null。

  • 如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)

13.6、ThreadLocalMap源码分析

1、基本结构

​ ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口用独立的方式实现了Map的功能,其内部的Entry也是独立实现

image-20210303214447305

2、成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 初始容量 —— 必须是2的整次幂
*/
private static final int INITIAL_CAPACITY = 16;

/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的冥。
*/
private Entry[] table;

/**
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子。
*/
private int size = 0;

/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0

/**
* 阈值设置为长度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

​ 与HashMap类似,INITIAL_CAPACITY代表这个Map数组的初始容量,table是一个Entry类型的数组,用于存储数据,size代表表中存储的数目,threshold代表扩容时对于size的阈值。

3、ThreadLocalMap中的存储结构Entry

ThreadLocalMap中的Entry类继承自WeakReference(弱引用),也就是key(ThreadLocal) 是弱引用,其目的是将ThreadLocal 对象的生命周期和线程生命周期解绑。

1
2
3
4
5
6
7
8
9
10
11
12
// 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了
// 另外,Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收

static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

4、弱引用和内存泄漏

​ 有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。

我们先来回顾这个问题中涉及的几个名词概念,再来分析问题

内存泄漏相关概念

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。
  • Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

弱引用相关概念

  • Java中的引用有4种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:

  • 强引用(StrongReference), 就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。

  • 弱引用(WeakReference),垃圾回收器一旦发现了 只具有弱弓|用的对象,不管当前内存空间足够与否,都会回收它的内存。

如果key使用强引用

  • 假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
    此时ThreadLocal的内存图(实线表示强引用)如下:

image-20210303220758608

如果key使用弱引用

image-20210303221126771

5、ThreadLocal内存泄漏总结

  • 如果ThreadLocalMap中的key使用强引用,那么当指向ThreadLocal的引用被回收后,key指向的ThreadLocal实例将无法被回收(因为key为强引用),此时无法避免内存泄漏

  • 如果ThreadLocalMap中的key使用弱引用,那么当指向ThreadLocal的引用被回收后,key指向的ThreadLocal实例直接被gc回收(因为key为弱引用),但由于此时Entry中的value不会被回收,因此仍然可能造成内存泄漏

​ 根据刚才的分析,我们知道了:无论使用ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏跟使用弱引用没有关系

​ ThreadLocal造成内存泄漏的根本原因是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应Entry就会造成内存泄漏。

​ 要避免内存泄漏有两种方式:

  • 使用完ThreadLocal ,调用其remove方法删除对应的Entry,就能避免内存泄漏
  • 使用完ThreadLocal ,当前Thread也随之运行结束相对第一种方式, 第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

也就是说,只要记得在使用完ThreadLocal及时的调用remove ,无论key是强引用还是弱引用都不会有问题。

6、key使用弱引用的原因

​ 事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (也即是ThreadLocal为null )进行判断如果key为nul的话,那么是会将value置为null的。
​ 这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

13.7、ThreadLocalMap解决hash冲突的方法

ThreadLocal使用的是自定义的ThreadLocalMap,接下来我们来探究一下ThreadLocalMap的hash冲突解决方式。

1、回顾ThreadLocal的set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

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

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
  • 获取当前线程,并获取当前线程的ThreadLocalMap实例(从getMap(Thread t)中很容易看出来)。
  • 如果获取到的map实例不为空,调用map.set()方法,否则调用构造函数 ThreadLocal.ThreadLocalMap(this, firstValue)实例化map。

可以看出来线程中的ThreadLocalMap使用的是延迟初始化,在第一次调用get()或者set()方法的时候才会进行初始化。

2、ThreadLocalMap构造方法

1
2
3
4
5
6
7
8
9
10
11
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//设置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}

主要说一下计算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

  • 关于& (INITIAL_CAPACITY - 1),这是取模的一种方式,对于2的幂作为模数取模,用此代替%(2^n),这也就是为啥容量必须为2的幂,在这个地方也得到了解答。
  • 关于firstKey.threadLocalHashCode
1
2
3
4
5
6
7
8
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647

这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENTHASH_INCREMENT = 0x61c88647,这个值和斐波那契散列有关(这是一种乘数散列法,只不过这个乘数比较特殊,是32位整型上限2^32-1乘以黄金分割比例0.618…的值2654435769,用有符号整型表示就是-1640531527,去掉符号后16进制表示为0x61c88647),其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。

3、ThreadLocalMap中的set方法

ThreadLocalMap使用开发地址-线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。

该方法一次探测下一个地址,直到有空的地址后插入若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

按照上面的描述,可以把table看成一个环形数组。

先看一下线性探测相关的代码,从中也可以看出来table实际是一个环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//计算索引
int i = key.threadLocalHashCode & (len-1);

/**
* 使用线性探测法查找元素
*/
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//table[i]上key不为空,并且和当前key相同,更新value
if (k == key) {
e.value = value;
return;
}
/**
* table[i]上的key为空,说明被回收了
* 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

笔记参考来源如下: