ThreadLocal

ThreadLocal 是什么

概述

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

  • synchronized 或者 lock ,有个管理员,好比现在大家签到,多个同学(线程),但是只有一只笔,只能同一个时间,只有一个线程(同学)签到,加锁(同步机制是以时间换空间,执行时间不一样,类似于排队)
  • ThreadLocal,人人有份,每个同学手上都有一支笔,自己用自己的,不用再加锁来维持秩序。以空间换时间,为每一个线程提供一份变量的副本,从而实现同时访问,互不干扰,效率自然比 lock 高;

api 介绍

  • protected T initialValue():initialValue() :返回此线程局部变量的当前线程的初始值。(对于 initialValue() 较为老旧,jdk1.8又加入了 withInitial() 方法)
  • static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) :创建线程局部变量
  • T get() :返回当前线程的此线程局部变量的副本中的值
  • void set(T value) :将当前线程的此线程局部变量的副本设置为指定的值
  • void remove() :删除此线程局部变量的当前线程的值

api 运用

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
package tech.chen.juccode.a15;
import sun.awt.windows.ThemeReader;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* @Date 2022/8/29 14:28
* @Author c-z-k
*/
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
House house = new House();
for (int i = 0; i < 4; i++) {
int i1 = new Random().nextInt(10)+1;
new Thread(()->{
try {
for (int i2 = 0; i2 < i1; i2++) {
house.saleCount();
house.saleCountByYourSelf();
}
System.out.println(Thread.currentThread().getName() +"\t 卖出 "+ house.threadLocalField.get());
} finally {
house.threadLocalField.remove();
}
},i+"").start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println("总共卖出" + house.houseCount);
}
}
class House{
public int houseCount;
public synchronized void saleCount(){
houseCount++;
}
public ThreadLocal<Integer> threadLocalField =ThreadLocal.withInitial(()->0);
public void saleCountByYourSelf(){
threadLocalField.set(threadLocalField.get()+1);
}
}

运行结果

0 卖出 1
2 卖出 1
3 卖出 2
1 卖出 7
总共卖出11

小总结:

  • 因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用

  • 既然其他 Thread 不可访问,那就不存在多线程共享的问题

  • 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的

  • 加入 synchronized 或者 lock 控制线程的访问顺序,而 ThreadLocal 人手一份,大家各自安好,没必要抢夺

ThreadLocal 分析

Thread、ThreadLocal、ThreadLocalMap 三者有何关联?

1661756056067

可见,Thread 中有一个 ThreadLocalMap 属性 ,点进 ThreadLocalMap

1661756118008

发现 ThreadLocalMap 是 ThreadLocal 的静态内部类

概括来讲:

  1. Thread类中有一个ThreadLocal.ThreadLocalMap threadLocals = null的变量,这个 ThreadLocal 相当于是 Thread 类ThreadLocalMap 的桥梁,在 ThreadLocal 中有静态内部类 ThreadLocalMap , ThreadLocalMap 中有 Entry 数组
  2. 当我们为 threadLocal 变量赋值,实际上就是以当前 threadLocal 实例为 key ,值为 value 的 Entry 往这个 threadLocalMap 中存放
  3. t.threadLocals = new ThreadLocalMap(this, firstValue) 如下这行代码,可以知道每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本

1661756275001

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//核心代码说明
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<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

set方法详解

  • 首先获取当前线程,并根据当前线程获取一个Map
  • 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
  • 如果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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
	/**
* 设置当前线程对应的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)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}

/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

/*
* firstKey : 本ThreadLocal实例(this)
* firstValue : 要保存的线程本地变量
*/
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);
}

get方法详解

先获取当前线程的 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 返回当前线程中保存 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对象
第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}

/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写, 如果不重写默认返回null
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)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}

remove方法详解

  • 首先获取当前线程,并根据当前线程获取一个 Map
  • 如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 entry
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);
}

ThreadLocal内存泄漏问题

为什么使用弱引用?

弱引用:只要 jvm 发生 gc ,弱引用对象就会被回收

  • 当方法执行完毕后,栈帧销毁强引用 tl 。但此时线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象

  • 若这个 key 引用是强引用,就会导致key指向的 ThreadLocal 对象及 v 指向的对象不能被 gc 回收,造成内存泄漏

  • 若这个 key 引用是弱引用就大概率会减少内存泄漏的问题。使用弱引用,就可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为null,这就解决了 ThreadLocal 内存泄露的问题,但是 entry 中的 value指向的对象呢?

1661820688043

key为null时的entry

ThreadLocalMap使用 ThreadLocal 的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,可以 key-value 结构中的 key为null了,但是 value 呢?

  • ThreadLocalMap 中就会出现 key 为null 的 Entry,根据源码我们知道,没有 ThreadLocal 我们是无法访问 value的,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些 key 为 null 的Entry的value就会一直存在一条强引用链,导致 value 指向的对象 长时间无法被回收。特别是使用线程池时;

  • 因此弱引用不能 100% 保证内存不泄露。我们要在不使用某个 ThreadLocal 对象后,手动调用 remmove 方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的 ThreadLocalMap对象也是重复使用的,如果我们不手动调用 remove 方法,那么后面的线程就有可能获取到上个线程遗留下来的 value 值,造成bug

  • 如果当前 thread 运行结束 ,threadLocal,threadLocalMap, Entry 没有引用链可达,在垃圾回收的时候都会被系统进行回收

  • 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在 Executors.newFixedThreadPool() 时创建线程的时候,为了复用线程是不会结束的,所以 threadLocal 内存泄漏就值得我们小心

另外要知道的是:set、get、remove方法会去检查所有键为null的Entry对象 ,当发现key 为 null 时,会去清除 value指向的对象

ThreadLocal小总结

  • ThreadLocal 本地线程变量,以空间换时间,线程自带的变量副本,人手一份,避免了线程安全问题

  • 每个线程持有一个只属于自己的专属 Map 并维护了 Thread Local 对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题

  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题

  • 都会通过 expungeStaleEntry,cleanSomeSlots, replace StaleEntry 这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法

  • 用完之后一定要 remove 操作