视频地址

https://www.bilibili.com/video/BV1N741127FH/?p=12&spm_id_from=333.880.my_history.page.click&vd_source=87b46b420aba8989ef39b883c7d29b91

作用

  • 提供线程内局部变量,不同线程之间不会相互干扰。

  • ThreadLocal 实例通常来说都是 private static 修饰的,用于关联线程和线程的上下文。

  • 减少同一个线程内的函数 或 组件之间传递变量的复杂性。

1
2
3
线程并发:多线程并发场景下,即单线程场景下用不上。
传递数据:如上红字。
线程隔离:每个线程变量都是独立的,不会相互影响。

基本使用

方法 描述
new ThreadLocal() 创建 ThreadLocal 对象
public void set(T value); 设置当前线程的绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量

ThreadLocal 是 Java 中的一个类,它提供了线程局部(thread-local)变量的能力。这些变量不同于它们的正常变量,因为每一个访问这个变量的线程都有其自己的独立初始化的变量副本。因此,通过 ThreadLocal 实例化的对象,只能被当前线程访问,其他线程无法访问和修改。

以下是 ThreadLocal 的基本使用步骤:

  1. 创建 ThreadLocal 实例

首先,你需要创建一个 ThreadLocal 的实例。你可以通过无参数的构造函数或提供初始值的构造函数来创建。

1
2
3
4
5
// 创建一个没有初始值的 ThreadLocal  
ThreadLocal<String> threadLocal = new ThreadLocal<>();

// 或者,创建一个带有初始值的 ThreadLocal
ThreadLocal<String> threadLocalWithInitialValue = ThreadLocal.withInitial(() -> "Initial Value");
  1. 设置线程局部变量的值

然后,你可以在你的线程中设置这个变量的值。这通常是通过调用 set(T value) 方法来完成的。

1
2
3
java复制代码

threadLocal.set("Thread-specific value");
  1. 获取线程局部变量的值

在你的线程中,你可以通过调用 get() 方法来获取这个变量的值。

1
2
3
java复制代码

String value = threadLocal.get(); // 返回 "Thread-specific value"
  1. 清理线程局部变量的值

在不再需要线程局部变量时,你应该清理它的值。这可以通过调用 remove() 方法来完成。但是,通常情况下,如果线程即将结束,那么它的所有 ThreadLocal 变量都会被自动清理。

1
2
3
java复制代码

threadLocal.remove();
  1. 注意事项
  • 使用 ThreadLocal 时要特别注意内存泄漏问题。如果一个线程长时间存活(比如线程池中的线程),并且 ThreadLocal 对象引用了大对象或大量对象,那么这些对象将不会被垃圾回收,从而导致内存泄漏。为了避免这种情况,你应该在不再需要 ThreadLocal 变量时调用 remove() 方法。
  • ThreadLocal 不应该用于跨线程传递数据或进行线程间的通信。它主要用于保存线程自己的状态信息。
  • ThreadLocal 是非线程安全的。每个线程都有自己的 ThreadLocal 变量副本,因此不需要额外的同步措施。但是,如果你在一个线程中修改了 ThreadLocal 变量的值,并期望其他线程看到这个修改,那么 ThreadLocal 将无法满足你的需求。

这就是 ThreadLocal 的基本使用方法。它对于保存线程特定的状态信息非常有用,但也需要谨慎使用以避免潜在的问题。


场景案例

JDBC事务 + ThreadLocal

事务要求:

  • 为了保证所有操作在一个事务中,一个线程下的 ServiceDao 层使用 Connection 必须是同一个。

  • 线程并发的情况下,每个线程只能操作各自的 Connection。(每个线程的Connection需要事务隔离)

最佳设计:

image.png


ThreadLocal 的内部结构

JDK1.7 之前的设计

image.png

JDK1.7 之后的设计

image.png

修改设计后的好处

  1. 每个Map 存储 Entry 的数量变少:就会尽量避免 Hash 冲突了,这样效率就变高了。

  2. 当 Thread 销毁的时候,ThreadLocal 也会随之销毁,减少内存的使用。反观 早先设计,Thread 结束,ThreadLocalMap 依然存在。


ThreadLocal 核心源码解读

以下代码块是上面四个方法的核心逻辑,为了保证思路清晰,ThreadLocalMap 部分暂时不展开讲,下一个知识点详解:

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
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 判断是否存在
if(map != null)
map.set(this, value);
else
// 1) 当前线程 Thread 不存在 ThreadLocalMap 对象.
// 2) 则调用 createMap 进行ThreadLocalMap 对象初始化.
// 3) 并将 t 和 value(对应的值) 作为第一个 entry 存放至 ThreadLocalMap 中
createMap(t, value);
}

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

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

/**
总结:
A. 首先获取当前线程,并根据当前线程获取一个Map.
B. 如果获取的Map不为空,则将参数设置到Map中(当前 ThreadLocal 作为 key)
C. 如果Map为空, 则给该线程创建 Map, 并设置初始值.
**/
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
30
31
32
33
34
35
36
public T get() {
// 获取当前线程对象 和 ThreadLocalMap;
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

// 如果 map 存在, 则直接返回 map 中 ThreadLocal 对应的值
if(map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);

if(e != null) {
T result = (T)e.getValue();
return result;
}
}

// map 不存在 或 map 中没 set 过
return setInitialValue();
}

private T setInitialValue() {
// 调用 initialValue 获取初始化的值
// initialValue 可以被子类重写,如果不重写, 默认返回 null
T value = initialValue();

// 获取当前线程对象 和 ThreadLocalMap;
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

if(map != null) // map 存在则设置默认值
map.set(this, value);
else
// 1) 当前线程 Thread 不存在 ThreadLocalMap 对象.
// 2) 则调用 createMap 进行ThreadLocalMap 对象初始化.
// 3) 并将 t 和 value(对应的值) 作为第一个 entry 存放至 ThreadLocalMap 中
createMap(t, value);
}
1
2
3
4
5
6
7
8
public void remove() {
// 获取当前线程中保存 ThreadLocalMap
ThreadLocalMap map = getMap(Thread.currentThread());

if(map != null)
// 存在则调用 map.remove 方法
map.remove(this);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 返回当前线程对应的 ThreadLocal 变量的 初始值.

* 此方法的第一次调用发生在, 当线程通过 get 方法访问此线程的 ThreadLocal 变量时
* 除非线程先调用 set 方法, 在这种情况下, initialValue 才不会被 get 方法调用
* 通常情况下, 每个线程最多调用一次这个方法

* <p> 这个方法仅仅简单的返回 null {@code null}
* 如果程序员想 ThreadLocal 线程局部变量有个除 null 以外的初始值,
* 必须通过子类继承 {@code ThreadLocal} 的方式去重写此方法.
* 通常, 可以通过匿名类的方式实现.

* @return 当前 ThreadLocal 的初始值
*/
protected T initialValue() {
return null;
}

⚠️ protected 表示同包,或者不同包的子类可继承、可重写。其既是为了让子类覆盖而设计的。


ThreadLocalMap 核心源码分析

在分析 `ThreadLocal` 方法的时候,我们了解到 `ThreadLocal` 的操作实际上时围绕 `ThreadLocalMap` 展开的。`ThreadLocalMap` 的源码相对比较复杂,我们从以下三个方面进行讨论。

基本结构

`ThreadLocalMap` 是 ThreadLocal 的内部类(很有意思的点,ThreadLocal的内部类,却在 Thread 内部维护),没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现的。

image-20240418204445918

(1) 成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 初始容量, 必须是 2 的整数次幂
*/
public static final int INITIAL_CAPACITY = 16;

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

/**
* 数组里面 entry 的个数, 可以用于判断 table 当前的使用量是否超过阈值
*/
private int size = 0;

/**
* 进行扩容的阈值, table 的使用量大于它的时候进行扩容.
*/
private int threshold;

(2)存储结构 Entry
ThreadLocalMap 中, 也是使用 Entry 来保存 K-V 结构数据的。不过 Entry 中的Key只能是 ThreadLocal 对象,这点在构造方法中已经定死了。
另外,Entry 继承自 WeakReference,也就是 key (ThreadLocal) 是弱引用,其目的是为了将 ThreadLocal 对象的生命周期 和 线程的生命周期解绑。

1
2
3
4
5
6
7
8
static class Entry extends WeakRefernece<ThreadLocal<?>> {
Object value;

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

弱引用和内存泄漏

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

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

(1)内存泄漏相关概念

  • Memory overflow:内存溢出,没有足够的内存给内存的申请者使用。

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

(2)弱引用相关概念:0

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

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

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

(3)如果 key 是强引用:

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

image.png

  1. 业务代码中使用了 ThreadLocal ,threadLocal Ref 被回收了。

  2. 但是因为 threadLocalMap 的 Entry 强引用了 ThreadLocal,造成 ThreadLocal 无法被回收。

  3. 在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,使用有强引用链 thread Ref → current Thread → threadLocalMap → entry , entry 就不会被回收(Entry当中包含了 ThreadLocal 实例和value), 导致 Entry 的内存泄漏。

也就是说:ThreadLocalMap 中的 key 使用了强引用,是无法避免内存泄漏的。

(4)Key 是弱引用:

当 ThreadLocal 中的Key使用了弱引用,会出现内存泄漏么?

image.png

  1. 同样假设业务代码中使用了 ThreadLocal,且 ThreadLocal Ref 被回收了。

  2. 由于 ThreadLocalMap 只持有 ThreadLocal Map 的弱引用,没有任何强引用指向 ThreadLocal实例,所以 threadLocal 就可以顺利被GC回收,此时 Entry 的 key = null.

  3. 但是在没有手动删除这个 Entry 以及 Current Thread 依然运行的前提下,在存在强引用链 threadRef → currentThread → threadLocalMap → entry → value , value 不会被回收,而这块 value 永远不会被访问到了,导致 value 的内存泄漏。

也就是说: ThreadLocalMap 的 key 使用了弱引用,也可能导致内存泄漏的问题。

(5)出现内存泄漏的真实原因:

比较以上两种情况,我们就会发现,内存泄漏的发生 跟 ThreadLocalMap 当中的key 是强还是弱引用没有关系,而造成真正的内存泄漏的原因如下:

  1. 没有手动删除这个Entry. (table 数组指定位置置为null, 断掉强引用链)

  2. CurrentThread 依然运行。(Current Thread 销毁,则指向 ThreadLocalMap的引用链就断了,进而引发后续链条上全部对象GC)

综上:ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 导致的(如果存在线程池,Thread 复用,则这个 ThreadLocalMap 永远不会销毁掉),如果没有手动删除对应的key,就会导致内存泄漏。

(6)为什么弱引用会导致内存泄漏,还是要使用弱引用呢:

事实上,ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为 null (即 ThreadLocal 为null)进行判断,如果为 null 的话,那么 value 也会置为 null。

这就意味着 使用完 ThreadLocal, CurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用可以比强引用多一层保障:弱引用的 ThreadLocal 会被回收,对应的value在下一次调用 set、get、remove 中任何一个方法的时候会被清除,从而避免内存泄漏。


Hash冲突的解决

hash 冲突的解决是Map中的一个重要内容。我们以hash冲突的解决方案为线索,来研究下ThreadLocalMap 的核心源码。

image-20240418203828673

构造方法:ThreadLocalMap(ThreadLocal<?> firstKey, Object value)
有不明白的地方看下上述 ThreadLocalMap 核心源码部分的当中关于成员属性的描述。

构造函数首先创建一个 16 大小 Entry 数组,然后计算出 firstKey 的索引,存储到 table 中,并设置 size 和 threshold;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ThreadLocalMap(ThreadLocal<?> firstKey, Object value) {
// 初始化 table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];

// 计算索引(重点):计算 fistKey 这个 ThreadLocal 在 table 中的存放位置
int i = firstKey.threadLocalHashCode & ( INITIAL_CAPACITY - 1);

// 创建 Entry 并放置到 table
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, value);
size = 1;

// 设置阈值
setThreshold(INITIAL_CAPACITY);
}

重点分析:int i = firstKey.threadLocalHashCode & ( INITIAL_CAPACITY - 1); 这一句中 hash 计算.

  • (a) 关于threadLocal.threadLocalHashCode

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private final threadLocalHashCode = nextHashCode();

    private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT); // 追加
    }

    // AutomicInteger 是一个提供原子操作的 Integer 类, 通过线程安全的方式操作加减,适合高并发的情况下使用.
    private static AutomicInteger nextHashCode = new AutomicInteger();

    // 特殊的 hash 值:和斐波拉契数列,黄金分割数是有关系的。
    private static final int HASH_INCREMENT = 0x61c88647;
          这里定义了一个 AutomicInteger 类型,每次当前值并加上 HASH_INCREMENT ,这个 HASH_INCREMENT 是和斐波拉契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是 Entry[] table 中,这样做可以尽量避免 hash 冲突。
    
  • (b) 关于 & ( INITIAL_CAPACITY - 1)

    计算索引位置的时候 采用了 hashcode & (size - 1) 的方式,相当于取余操作 hashCode % size 的一个更高效的实现,正是因为这种算法,我们要求 size 必须是 2 的整数次幂,这也能保证在索引不越界的情况下,使得 hash 发生冲突的次数减小。

ThreadLocalMap 的 set 方法:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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();

// ThreadLocal 对应的key 存在,则覆盖之前的值
if(k == key) {
e.value = value;
return;
}

// key 为 null, 但是值不为 null (未及时 remove, 但之前放置此位置 ThreadLocal 被回收的情况), 当前数组中的 Entry 是一个成旧的 (stale) 元素.
if(k == null) {
// 用新的元素替换成旧的元素,这个进行了不少的垃圾清理动作, 防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}

// ThreadLocal 对应的 key 不存在,且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry
tab[i] = new Entry(key, value);
int sz = ++size;

/**
* cleanSomeSlots 用于清除那些 e.get() == null 的元素(即, key 为 null 的元素).
* 这种数据 key 关联的对象已经被回收了,所以这个 Entry(table[index]) 可以被置为 null,
* 如果没有清除任何 entry, 并且当前使用量的达到了 负载因子 所定义的 2/3, 那么进行 rehash (执行一次全表的,扫描清理工作)
*/
if(!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

/**
* 获取环形索引的下一个数组
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0;)
}
  • 执行流程简述:

    1. 首先根据 key 计算索引 i, 然后查找 i 位置的上 Entry。

    2. 若是 Entry 已经存在,并且 key 等于传入的 key,那么这个时候直接给这个 Entry 赋新的 value 值。

    3. 若是 Entry 已经存在,并且 key 为 null,则调用的 replaceStaleEntry 来更换这个 key 为空 Entry

    4. 不断循环检测,直到遇见为 null 的地方,这个时候要是还没在循环中 return,那么就在这个 null 的位置重新建一个Entry,并且插入,size + 1。

    5. 最后调用 cleanSomeSlots,清理 key 为 null 的Entry,最后返回是否清理了 Entry,接下来判断 sz 是否 ≥ threshold 达到 rehash 条件,达到的话就会调用 rehash 函数执行一次全表的清理工作。

  • 重点分析:ThreadLocalMap 使用 线性探测法 来解决 hash 冲突:

    该方法一次探测一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

    举个例子:假设当前的 tab 长度为 16,也就是说如果计算出来 key 的索引为 14,如果 table[14] 上已经有值,并且 key 与当前 key 不一致,那么就发生了hash冲突,这个时候将 14 + 1 得到 15,取 table[15] 进行判断,这个时候如果还是冲突就会返回 0 ,取 table[0],依次类推,直到可以插入。按照上面这个描述,可以把 Entry[] table 看成是一个环形数组。

你了解ThreadLocal变量吗

通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有 ⾃⼰的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程拥有只属于自己的专属本地变量,**如果你创建了⼀个 ThreadLocal 类形象的⽐喻成 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本 **,这也是 ThreadLocal 变量名的由来。他们可以使⽤ **get 和 set **⽅法来获取默认值 或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题