1. ThreadLocal
是什么
ThreadLocal
用于解决多线程环境下的线程安全问题,ThreadLocal
有两个使用场景:
- 线程封闭:
ThreadLoal
为每个线程创建一个独享的变量副本 (线程间数据隔离),每个线程只能修改自己所持有的变量副本而不会影响其他线程的变量副本,确保了线程安全; - 上下文传递:
ThreadLocal
保存的信息,可以被同一个线程内的不同方法访问,可以使用ThreadLocal
传递上下文信息,减少方法之间的传参 (类似于全局变量);
2. ThreadLocal
如何使用
API | 描述 |
---|---|
ThreadLocal() |
构造函数,创建ThreadLocal 对象 |
get():T |
获取当前线程绑定的局部变量 |
set(value:T) |
设置当前线程绑定的局部变量 |
remove() |
移除当前线程绑定的局部变量 |
使用案例
java
public class ThreadLocalSimple {
private static ThreadLocal<String> sThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 1.开启五个线程,同时向 sThreadLocal 中设置不同的值
// 2.随后从 sThreadLocal 中读取设置的数据
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
String threadName = Thread.currentThread().getName();
// 将当前线程的名称设置到 sThreadLocal 中
sThreadLocal.set(Thread.currentThread().getName());
System.out.println(threadName + "-> getFromThreadLocal:" + sThreadLocal.get());
});
thread.setName("thread-" + i);
thread.start();
}
}
}
// 输出
thread-2-> getFromThreadLocal:thread-2
thread-4-> getFromThreadLocal:thread-4
thread-3-> getFromThreadLocal:thread-3
thread-1-> getFromThreadLocal:thread-1
thread-0-> getFromThreadLocal:thread-0
从结果来看可以发现,每个线程都能够从同一个对象 sThreadLocal
中正确的获取到设置的内容;
3. ThreadLocal
与 Synchronized
关键字
ThreadLocal
模式和 Synchronized
关键字都是用于处理多线程并发访问变量的问题,但是两者处理问题的方式不同,具体如下:
ThreadLocal |
Synchronized |
|
---|---|---|
原理 | ThreadLocal 采用以 空间换时间 的方式,为每个线程都提供一份变量副本,从而实现同时访问不相干扰 |
同步机制采用以 时间换空间 的方式,只有一份变量,让不同的线程排队访问 |
侧重点 | 多线程中,让每个线程之间的 数据相互隔离 | 多线程访问资源的同步 |
简单来说
Synchronized
通过锁机制让不同的线程排队访问同一个变量,来保证线程安全问题,用于线程间的数据共享问题;ThreadLocal
为每个线程创建一个变量副本,每个线程访问自己的变量副本,来保证每个线程访问的数据不相干扰,用于线程间的数据隔离;
4. Java
中的引用
Java
中的引用类型有四种,根据引用强度的由强到弱,分别是: 强引用、软引用、弱引用、虚引用;
引用类型 | GC回收时机 | 使用示例 |
---|---|---|
强引用 Strong Reference |
如果一个对象具有强引用,那垃圾回收器绝不会回收该对象 | Object obj = new Object(); |
软引用 Soft Reference |
在内存不足时,会对软引用对象进行回收 | SoftReference<Object> softObj = new SoftReference(); |
弱引用 Weak Reference |
第一次 GC 回收时,如果垃圾回收器遍历到此弱引用,则将该弱引用对象回收 |
WeakReference<Object> weakObj = new WeakReference(); |
虚引用 Phantom Reference |
一个对象是否有虚引用的存在,不会对其生存的时间产生影响,同时也无法从一个虚引用来获取一个对象的实例 | 不会使用 |
5. Thread
、TheadLocalMap
、ThreadLocal
之间的关系
从 JDK1.8
开始,ThreadLocal
的内部结构改成: 每个 Thread
维护一个 ThreadLocalMap
, 这个 map
存储的 key
为 ThreadLocal
对象,value
为真正要存储的值,具体如下:
- 每个
Thread
线程内部都有一个ThreadLocalMap
类型的变量thread.threadLocals
默认为null; ThreadLocalMap
存储的键值对分别为ThreadLocal
(key) 和 线程的变量副本 (value);Thread
内部的ThreadLocalMap
是由ThreadLocal
维护的,由ThreadLocal
负责向map
获取和设置线程的变量值;- 对于不同的线程,通过
ThreadLocal
(key),获取到的只能是线程自己维护的变量副本,从而实现副本数据的相互隔离;
为什么是这个结构
- 实际开发过程中,
Thread
的数量往往会多于ThreadLocal
的数量,这样设计后会减少Entry
的数量; - 当
Thread
结束后,对应的ThreadLocalMap
也会随之销毁;
6. ThreadLocal
源码分析
6.1 常用函数解析
6.1.1 void set(T value)
设置当前线程对应的ThreadLocal
的值,value
为要保存在当前线程中的值,流程如下:
- 获取当前线程
Thread
; - 获取当前线程对应的
ThreadLocalMap
对象threadLocals
;threadLocals != null
: 将参数value
存储到map
中,key
为ThreadLocal
;threadLocals == null
: 创建ThreadLocalMap
对象,并赋初始值value
;
java
// java.lang.ThreadLocal
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程 t 对应的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 成功获取到 map 对象时,保存对应的键值对
map.set(this, value);
} else {
// 首次访问 map 为null,走map创建流程
createMap(t, value);
}
}
// 返回线程t对应的 ThreadLocalMap 对象,首次获取返回 null
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 为线程 t 创建 ThreadLocalMap 对象,并赋初始值 firstValue
void createMap(Thread t, T firstValue) {
// 这里的 this 表示 ThreadLocal 对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
6.1.2 T get()
- 获取当前线程
Thread
; - 获取当前线程对应的
ThreadLocalMap
对象threadLocals
; - 获取
key
为当前ThreadLocal
对象的value
值; (假设:threadLocals != null
) - 返回对应的
value
;(假设ThreadLocal
对应的value != null
)
上述 2、3两步失败时,则会触发 ThreadLocal.initialValue()
函数,并根据默认的初始值更新 ThreadLocal
对应的 value
,最后返回初始值;
java
// java.lang.ThreadLocal
// 返回当前线程中保存 ThreadLocal 的值
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程 t 对应的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 成功获取到 ThreadLocalMap 对象时,根据key (ThreadLocal) 获取存储的value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 成功获取到key (ThreadLocal) 存储的value时,返回对应值
T result = (T)e.value;
return result;
}
}
// 1. 线程 t 还未创建 ThreadLocalMap 对象
// 2. ThreadLocalMap 没有成功获取到 ThreadLocal key 对应的value时
// 走下面的初始化流程
return setInitialValue();
}
// 初始化ThreadLocal并返回初始化的值
private T setInitialValue() {
// 调用 ThreadLocal 的初始化函数
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程 t 对应的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// map存在,存储初始值value
map.set(this, value);
} else {
// map不存在,创建ThreadLocalMap并存储初始值value
createMap(t, value);
}
// 返回初始值
return value;
}
6.1.3 void remove()
删除当前线程存储的 ThreadLocal
对应的 Entry
;
java
// java.lang.ThreadLocal
public void remove() {
// 获取当前线程对应的 ThreadLocalMap 对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 删除key为当前 ThreadLocal 的对应的 entry
m.remove(this);
}
}
6.1.4 protected T initialValue()
返回当前线程对应的 ThreadLocal
的初始值,此方法的第一次调用发生在,线程通过 get()
方法访问此线程的 ThreadLocal
值时,如果该线程先调用了 set()
方法,那么理论上 initialValue()
方法不会被触发;
java
// java.lang.ThreadLocal
protected T initialValue() {
// 默认返回 null,如果需要返回默认值时,需要子类重写该方法
return null;
}
6.2 ThreadLocalMap
可以看到 ThreadLocal
的操作实际上是围绕 ThreadLocalMap
对象展开的
6.2.1 ThreadLocalMap
的基本结构
ThreadLocalMap
是 ThreadLocal
的静态内部类,没有实现 Map
接口,用独立的方式实现了 Map
的功能,结构图如下:
ThreadLocalMap
将 key-value
封装在 Entry
对象中,然后将 Entry
对象存储在类型为 Entry[]
的成员变量 table
中;
6.2.2 关于 Entry
ThreadLocalMap
存储的 Entry
为 ThreadLocalMap
的静态内部类,继承自 WeakReference
弱引用类,同时限定了弱引用持有的对象类型为 ThreadLocal<?>
类型;
Entry
存储的 key-value
结构中,弱引用持有 key
(也就是ThreadLocal
) 对象,其目的是将 ThreadLocal
对象的生命周期和线程的生命周期解绑;
java
// java.lang.ThreadLocal
// 限定了弱引用的数据类型为 ThreadLocal
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
6.2.3 ThreadLocalMap
的创建
从 ThreadLocal
的常用函数可以发现,Thread
并没有在初始化的时候创建 ThreadLocalMap
对象,而是在 ThreadLocal
调用 set()/get()
时,通过 createMap(Thread, T)
创建;
java
// java.lang.ThreadLocal
// 为线程 t 创建 ThreadLocalMap 对象并设置第一个值 firstValue
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
// 初始容量 (注意: 容量大小需要为2的幂次方)
private static final int INITIAL_CAPACITY = 16;
// 存放数据的数组
private Entry[] table;
// 数组里面真是存储的数据数量,可用于判断table当前使用量是否达到了阈值
private int size = 0;
// table用于判断是否需要扩容的阈值
private int threshold; // Default to 0
// 构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 默认创建一个初始容量为16的 Entry 数组
table = new Entry[INITIAL_CAPACITY];
// 计算key的哈希值,然后通过 & 运算得到一个 [0, INITIAL_CAPACITY - 1] 区间的数字
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 使用上述计算出的index,存储 key-value 键值对对象 Entry
table[i] = new Entry(firstKey, firstValue);
// 更新 size 大小
size = 1;
// 更新默认的阈值
setThreshold(INITIAL_CAPACITY);
}
...
}
ThreadLocalMap
构造函数首先创建了一个长度为 16 的 Entry
数组,然后通过 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
计算得到一个 [0, table.length - 1]
区间内的 index
下标 (这里 table.length = 16
);
6.2.4 Entry[]
的 index 计算
从 ThreadLocalMap
的构造函数中可以看到,Entry[]
通过 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
计算出 Entry
在 Entry[]
中的 index;
这里的 firstKey
就是 ThreadLocal
对象,threadLocalHashCode
为一个 16 进制的数值,默认值为常量 HASH_INCREMENT = 0x61c88647
;
java
// java.lang.ThreadLocal
// 这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里
// 也就是Entry[] table中,这样做可以尽量避免hash冲突。
private static final int HASH_INCREMENT = 0x61c88647;
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
// nextHashCode 默认为0,因此返回的初始值为 0x61c88647
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 创建一个原子操作的 Integer 类,初始值为 0
private static AtomicInteger nextHashCode = new AtomicInteger();
& (INITIAL_CAPACITY - 1)
计算 hash 的时候采用 hashCode & (table.length - 1)
的算法,相当于取模运算(hashCode % table.length
)得到一个 [0, table.length - 1]
区间内的值;
6.2.5 ThreadLocalMap
添加 Entry
ThreadLocal
的 threadLocalHashCode
字段会通过静态函数 nextHashCode()
获取到一个 16 进制数值,由于 nextHashCode()
是一个静态函数,且每次调用时都会执行一次 add
操作,因此每个 ThreadLocal
对象获取到的 threadLocalHashCode
为不同的 16 进制数值;
ThreadLocalMap
添加新的 key-value
之前,会先通过 ThreadLocal.threadLocalHashCode
计算出 key
对应 table[]
的 index
,这里会有几种情况:
table[index] != null && table[index].key == key
:index
对应位置已经存在entry
对象,且entry.key
对象和要存储的key
是同一个对象,此时直接将新的value
赋值给entry.value
;table[index] != null && table[index].key == null
:index
对应位置已经存在entry
对象,但是entry.key
已经被回收,此时index
位置可以被重用;table[index] != null && table[index].key != null
: 表示该位置已经存储了一个有效的entry
(哈希冲突),通过nextIndex(index, len)
向后推一个位置,此时有两种情况:index + 1 < len
: 此时新的index
没有数组越界,使用新的index
执行 步骤1和2的判断;index + 1 = len
: 此时新的index
发生了数组越界,此时返回 0 值,执行步骤1和2的判断;
table[index] == null
: 表示该位置是一个还未被使用的index
,此时直接向该位置插入新的Entry
对象;
java
// java.lang.ThreadLocal
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 通过key的哈希值计算出当前key在 table 数组中的 index
int i = key.threadLocalHashCode & (len-1);
// 由于哈希冲突的存在,不同key会计算出相同的 index
// 这里 for循环从 index = i 位置开始遍历取出缓存的 Entry 对象
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 获取到table数组中缓存的 Entry 的 key (ThreadLocal)
ThreadLocal<?> k = e.get();
if (k == key) {
// 如果当前 index 缓存的 entry.key 跟需要设置的 ThreadLocal 相等,直接更新 entry 对象的 value
e.value = value;
return;
}
if (k == null) {
// key 为弱引用对象,key == null 说明原先的 ThreadLocal 被回收了
// 此时 table[i] 位置可以被当前 key-value 复用
replaceStaleEntry(key, value, i);
return;
}
}
// index = i 位置不存在 entry 或者没有可以复用的 index,直接创建一个 Entry 对象
tab[i] = new Entry(key, value);
// 更新 entry 数量
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容
rehash();
}
7. ThreadLocal
中的内存泄露问题
Thread
、ThreadLocal
、ThreadLocalMap
之间的引用关系如图:
内存泄露原因
从引用关系图可以看到 ThreadLocalMap
作为 Thread
的属性,其生命周期是跟 Thread
一样长,假设 ThreadLocal
被回收,而线程还未结束,那么 ThreadLocalMap
中对应的 Entry.key
会被置为 null
,此时这个 entry.value
在线程生命周期内不会再次被访问,如果线程是复用的,那么该 ThreadLocalMap
内部就会存在一个或多个 entry(null, value)
对象,从而导致内存泄漏;
如何解决
- 将
ThreadLocal
对象声明为static
,让ThreadLcoal
的生命周期更长,此时就会一直存在强引用; - 每次使用完
ThreadLocal
对象,及时的调用ThreadLocal.remove()
方法清除数据;