文章目录
- 一.线程隔离
- 二.ThreadLocal是什么
- 三.ThreadLocal类提供的方法
- 四.入门使用
- 五.ThreadLocal
- 1.ThreadLocal的数据结构
- 2.ThreadLocal.ThreadLocalMap
- 3.ThreadLocal 的 get()方法解析
- 4. ThreadLocal 的initialValue()解析
- 5.ThreadLocal 的 set()解析
- 5.remove()
- 六.ThreadLocal 内存泄漏
- 七.ThreadLocal 应用场景
- 八.可继承的ThreadLocal-InheritableThreadLocal
- 九.为什么建议使用static修饰ThreadLocal?
- 十.ThreadLocal的注意事项
- 十一.ThreadLocal原理总结
一.线程隔离
当多线程访问时,通过将数据封闭在各自的线程中相互隔离,互不干扰的技术称为线程隔离,ThreadLocal
就是线程隔离的一种体现
二.ThreadLocal是什么
- ThreadLocal类提供了一种
线程局部变量(ThreadLocal)
,即每一个线程都会保存一份变量副本,每个线程都可以独立地修改自己的变量副本,而不会影响到其他线程
- ThreadLocal 变量通常被private static修饰,其中保存变量属于当前线程,该变量对其他线程而言是隔离的,当一个线程结束时,它所使用的所有 ThreadLocal 实例副本都可被回收。
- ThreadLocal 适用于变量在线程间隔离而在方法或类间共享的场景。
- ThreadLocal唯一的缺点就是:只能用于存储当前线程的变量。
子线程获取不到父线程的数据
(使用InheritableThreadLocals`可以解决)
三.ThreadLocal类提供的方法
方法 | 描述 |
---|---|
public T get() | 获取ThreadLocal在当前线程中保存的变量副本 |
public void set(T value) | 设置当前线程中变量的副本 |
public void remove() | 移除当前线程中变量的副本 |
protected T initialValue() | initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法, 返回此线程局部变量当前副本中的初始值 |
ThreadLocalMap是ThreadLocal的静态内部类
,该类才是实现线程隔离机制的关键。,get()、set()、remove()方法底层都是对该内部类进行操作,ThreadLocalMap
用键值对方式存储每个线程变量的副本,key
为当前ThreadLocal对象
,value
为对应线程的变量副本
。
四.入门使用
假设每个线程都需要一个计数器记录自己做某件事做了多少次,各线程运行时都需要改变自己的计数值而且相互不影响,那么ThreadLocal就是很好的选择,这里ThreadLocal里保存的当前线程的局部变量的副本就是这个计数值。
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {//初始化值为0
return 0;
}
};
//当前统计 + 1,然后返回
public int nextSeq() {
seqCount.set(seqCount.get() +1);
return seqCount.get();
}
public static void main(String [] args) {
SeqCount seqCount = new SeqCount();
//开启四个线程
SeqThread seqThread1 = new SeqThread(seqCount);
SeqThread seqThread2 = new SeqThread(seqCount);
SeqThread seqThread3 = new SeqThread(seqCount);
SeqThread seqThread4 = new SeqThread(seqCount);
seqThread1.start();
seqThread2.start();
seqThread3.start();
seqThread4.start();
}
//静态内部类,循环调用3次seqCount.nextSeq()方法
public static class SeqThread extends Thread {
private SeqCount seqCount;
public SeqThread(SeqCount seqCount) {
this.seqCount = seqCount;
}
@Override
public void run() {
for (int i=0; i<3; i++) {
System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());
}
}
}
}
执行结果:
五.ThreadLocal
1.ThreadLocal的数据结构
-
Thread类
有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,也就是说每个线程有一个自己的ThreadLocalMap。 -
ThreadLocalMap
有自己的独立实现,可以简单地将它的key视作ThreadLocal
,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用
)。 -
每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在
自己的ThreadLocalMap
里找对应的key,从而实现了线程隔离。 -
ThreadLocalMap
类似于HashMap的结构,只是·HashMap是由数组+链表(数组组+链表+红黑树结构,当链表长度大于8,转为红黑树)
实现的,而ThreadLocalMap
中并没有链表结构,使用Entry
来保存键值对, 它的key是ThreadLocal<?> k
,继承自WeakReference
, 也就是我们常说的弱引用类型,在发生GC时会被回收。扫描二维码关注公众号,回复: 11382388 查看本文章
1.1.Java的四种引用类型
Java有四种引用类型,引用强度从强到弱依次为:强引用、软引用、弱引用和虚引用
-
强引用
:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候 -
软引用
:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收 -
弱引用
:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收 -
虚引用
:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
1.2.GC之后,Entry的key是否是null?
ThreadLocal 是弱引用,那么在threadLocal.get()的时候,发生GC之后,key是否是null?
我们使用反射的方式来看看GC后当前线程的ThreadLocalMap
的数据情况:
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
//开启线程1并调用test()方法
Thread test1 = new Thread(() -> test("11111", false),"test1");
test1.start();
//当前线程t等待主线程执行完在执行
test1.join();
System.out.println("--gc后--");
//开启线程1并调用test()方法且调用GC()
Thread test2 = new Thread(() -> test("22222", true),"test2");
test2.start();
//当前线程t2等待主线程执行完在执行
test2.join();
}
/**
* 测试GC和非GC时当前线程的ThreadLocal是否被回收
* @param s 字符串
* @param isGC 是否调用GC true调用/false不调用
*/
private static void test(String s, boolean isGC) {
try {
//当前线程设置ThreadLocal
new ThreadLocal<>().set(s);
//发起GC
if (isGC) {
System.gc();
}
// 获取当前线程
Thread currentThread = Thread.currentThread();
// 获取当前线程的class
Class<? extends Thread> clazz = currentThread.getClass();
// 获取当前线程的threadLocals属性对象
Field field = clazz.getDeclaredField("threadLocals");
// 设置threadLocals字段的可见级别
field.setAccessible(true);
//获取当前线程对象的threadLocals的属性值
Object threadLocalMap = field.get(currentThread);
// 获取当前线程 threadLocals属性的class(ThreadLocal.ThreadLocalMap)
Class<?> tlmClass = threadLocalMap.getClass();
// 获取当前线程ThreadLocal.ThreadLocalMap类内的table属性对象
Field tableField = tlmClass.getDeclaredField("table");
// 设置ThreadLocal.ThreadLocalMap类内table字段的可见级别
tableField.setAccessible(true);
// 获取当前线程的ThreadLocal.ThreadLocalMap类的 table字段的属性值 (Entry[] table)
Object[] entryArr = (Object[]) tableField.get(threadLocalMap);
//遍历ThreadLocal.ThreadLocalMap.table属性 (Entry[] table)
for (Object entry : entryArr) {
if (entry != null) {
//获取当前entry的class
Class<?> entryClass = entry.getClass();
//获取当前entry的value字段并设置字段可见性
Field valueField = entryClass.getDeclaredField("value");
valueField.setAccessible(true);
//获取当前entry的key字段并设置字段可见性
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
referenceField.setAccessible(true);
//打印key/value
System.out.println(String.format("ThreadName=[%s],弱引用key=[%s],值=[%s]", currentThread.getName(),referenceField.get(entry), valueField.get(entry)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果:
test1线程Debug详情
test2线程Debug详情
结论:ThreadLocal是弱引用,在发生GC时会自动会回收掉,但如果ThreadLocal对应的value是强引用则不会被回收,也就是会出现我们 value 没被回收,key 被回收,导致 value 永远存在,出现内存泄漏问题。
1.3.ThreadLocal重要属性
// 当前 ThreadLocal 的 hashCode,由 nextHashCode() 计算而来,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 哈希魔数,主要与斐波那契散列法以及黄金分割有关
private static final int HASH_INCREMENT = 0x61c88647;
// 保证了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode = new AtomicInteger();
/*
* threadLocalHashCode`是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了
* 生成过程则是调用`nextHashCode():`
*/
// 返回计算出的下一个哈希值,其值为 i * HASH_INCREMENT,其中 i 代表调用次数
private static int nextHashCode() {
//该函数简单地通过一个增量HASH_INCREMENT来生成hashcode。
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,
//HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCod的增量。
/*
至于为什么这个增量为0x61c88647?
主要是因为ThreadLocalMap的初始大小为16,每次扩容都会为原来的2倍,这样它的容量永远为2的n次方,
该增量选为0x61c88647也是为了尽可能均匀地分布,减少碰撞冲突。
*/
其中的 HASH_INCREMENT
也不是随便取值的
- 转换为十进制是
1640531527,2654435769
- 转换成 int 类型就是
-1640531527,2654435769
- 等于
(√5-1)/2 乘以 2 的 32 次方
。(√5-1)/2
就是黄金分割数,近似为0.618
,也就是说0x61c88647
可以理解为一个黄金分割数乘以 2 的 32 次方
,它可以保证nextHashCode
生成的哈希值,均匀的分布在2 的幂次方
上,且小于 2 的 32 次方
。
示例代码:
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) throws Exception {
int n = 5;
int max = 2 << (n - 1);
for (int i = 0; i < max; i++) {
System.out.print(i * HASH_INCREMENT & (max - 1));
System.out.print(" ");
}
}
运行结果为:0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25
.
可以发现元素索引值完美的散列在数组当中,并没有出现冲突。
2.ThreadLocal.ThreadLocalMap
- 因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
- ThreadLocalMap使用用
Entry类
来进行存储,key为当前ThreadLocal对象的引用
,value为我们要存储的值,
-我们使用的ThreadLocal.get()、ThreadLocal.set(),ThreadLocal.remove()
方法其实都是底层先获取这个这个ThreadLocalMap
,然后调用这个map对应的 get()、set() ,remove()来实现增删改查。
源码如下:
static class ThreadLocalMap {
/**
* 键值对实体的存储结构
* Entry继承WeakReference,所以Entry对应key的引用(ThreadLocal实例)是一个弱引用。)
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
// 当前线程关联的 value,这个 value 并没有用弱引用追踪
Object value;
/**
* 构造键值对
*
* @param k k 为 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
* @param v v 为 value
*/
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量,必须为 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
private Entry[] table;
// ThreadLocalMap 元素数量
private int size = 0;
// 扩容的阈值,默认是数组大小的三分之二(1.5倍)
private int threshold;
//----------------省略其他代码-------------
}
源码中发现 ThreadLocalMap
就是一个简单的 Map 结构,底层是数组
,有初始化大小
,也有扩容阈值大小
,数组的元素是 Entry
,Entry 的 key 就是 ThreadLocal 的引用
,value 是 ThreadLocal 的值
。ThreadLocalMap 解决 hash 冲突的方式采用的是 线性探测法
,如果发生冲突会继续寻找下一个空的位置。
2.1.ThreadLocalMap.set()解析
/**
* key 当前threadLocal的引用
* value 要存储的值
*/
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
//获取Entry数组长度
int len = tab.length;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置(计算 key 在数组中的下标)
int i = key.threadLocalHashCode & (len-1);
// 采用“线性探测法”,寻找合适位置(索引处为空即是合适的位置)(如果发生冲突会继续寻找下一个空的位置)
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i] ; e != null; e = tab[i = nextIndex(i, len)]) {
//获取该哈希值处的ThreadLocal对象
ThreadLocal<?> k = e.get();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
if (k == null) {
// 用新元素替换陈旧的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清楚陈旧的Entry(key == null)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值(数组大小的三分之二),则进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容的过程也是对所有的 key 重新哈希的过程
rehash();
}
/**
* 索引位置 + 1
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
-
ThreadLocalMap.set()方法和Map.put()方法差不多,但是有一点区别是:Map.put方法处理哈希冲突使用的是
链地址法
,而set方法使用的开放地址法
。 -
ThreadLocalMap.set()中的
replaceStaleEntry()
和cleanSomeSlots()
,这两个方法可以清除掉key ==null的实例
,防止内存泄漏
。
2.2.ThreadLocalMap.getEntry()解析
/**
* 当前ThreadLocal的引用
*/
private Entry getEntry(ThreadLocal<?> key) {
//// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置(计算 key 在数组中的下标)
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
由于采用了开放定址法,所以当前key的散列值和元素
在数组中的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss()
,如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这里有一个重要的地方,当key==null
时,调用了expungeStaleEntry()
方法,该方法用于处理key == null
,有利于GC回收,能够有效地避免内存泄漏。
3.ThreadLocal 的 get()方法解析
public T get() {
// 返回当前 ThreadLocal 所在的线程
Thread t = Thread.currentThread();
// 从线程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 ThreadLocalMap 中拿到 entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不为空,读取当前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value
return setInitialValue();
}
//初始化当前线程的 ThreadLocal
private T setInitialValue() {
T value = initialValue();//initialValue不重写默认返回null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//getMap()方法可以获取当前线程所对应的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//初始化ThreadLocal方法
protected T initialValue() {
return null;
}
//初始化当前线程的ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化Entry数组容量
table = new Entry[INITIAL_CAPACITY];
//初始化第一个key的索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//第一个Entry的存储
table[i] = new Entry(firstKey, firstValue);
//长度为1
size = 1;
//设置ThreadLocalMap的长度
setThreshold(INITIAL_CAPACITY);
}
//设置ThreadLocalMap的长度
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
get 方法的主要流程为:
- 先获取到当前线程的
引用
- 获取
当前线程内部
的ThreadLocalMap
- 如果 ThreadLocalMap 存在,通过ThreadLocalMap的getEntry()方法 获取当前 ThreadLocal 对应的 value 值
- 如果 ThreadLocalMap 不存在或者找不到 value 值,则调用
setInitialValue() 进行初始化
get 方法的时序图如下所示:
其中每个 Thread 的ThreadLocalMap
以 threadLocal
作为key
,保存自己线程的 value 副本,是保存在每个线程中,并没有保存在 ThreadLocal 对象中。
其中 ThreadLocalMap.getEntry() 方法的源码如下:
/**
* 返回 key 关联的键值对实体
*
* @param key threadLocal
* @return
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回
if (e != null && e.get() == key) {
return e;
} else {
// 从 i 开始向后遍历找到键值对实体
return getEntryAfterMiss(key, i, e);
}
}
ThreadLocalMap 的 resize 方法
当 ThreadLocalMap 中的 ThreadLocal 的个数超过容量阈值时,ThreadLocalMap 就要开始扩容了,我们一起来看下 resize 的源代码:
/**
* 扩容,重新计算索引,标记垃圾值,方便 GC 回收
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
// 新建一个数组,按照2倍长度扩容
Entry[] newTab = new Entry[newLen];
int count = 0;
// 将旧数组的值拷贝到新数组上
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 若有垃圾值,则标记清理该元素的引用,以便GC回收
if (k == null) {
e.value = null;
} else {
// 计算 ThreadLocal 在新数组中的位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果发生冲突,使用线性探测往后寻找合适的位置
while (newTab[h] != null) {
h = nextIndex(h, newLen);
}
newTab[h] = e;
count++;
}
}
}
// 设置新的扩容阈值,为数组长度的三分之二
setThreshold(newLen);
size = count;
table = newTab;
}
resize 方法主要是进行扩容,同时会将垃圾值标记方便 GC 回收,扩容后数组大小是原来数组的两倍。
4. ThreadLocal 的initialValue()解析
protected T initialValue() {
return null;
}
在上面的代码分析get()的过程中,我们发现如果没有先set
的话,即在ThreadLocalMap
中查找不到对应的存储,则会通过调用setInitialValue
方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue()
, 而默认情况下,initialValue方法返回的是null
。
该方法定义为protected
级别且返回为null
,所以我们在使用ThreadLocal的时候一般都应该重写该方法。
注意:如果想在get之前不需要调用set就能正常访问的话,必须
重写initialValue()
方法。
5.ThreadLocal 的 set()解析
public void set(T value) {
// 返回当前ThreadLocal所在的线程
Thread t = Thread.currentThread();
// 返回当前线程持有的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
map.set(this, value);
} else {
// 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set 方法的作用是把我们想要存储的 value 给保存进去。set 方法的流程主要是:
- 先获取到
当前线程的引用
- 利用这个引用来获取到
ThreadLocalMap
- 如果 ThreadLocalMap 为空,则去创建一个 ThreadLocalMap
- 如果 ThreadLocalMap 不为空,就利用
ThreadLocalMap 的 set 方法
将 value 添加到 map 中
通过createMap可以看出最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装
,传递了变量值
set 方法的时序图如下所示:
其中map
就是我们上面讲到的ThreadLocalMap
,可以看到它是通过当前线程对象获取到的 ThreadLocalMap,接下来我们看 getMap方法的源代码:
/**
* 返回当前线程 thread 持有的 ThreadLocalMap
*
* @param t 当前线程
* @return ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
getMap 方法的作用主要是获取当前线程内的 ThreadLocalMap 对象
,可以看出,原来 threadLocals 是线程的一个属性
所以在多线程环境下 threadLocals 是线程安全的,下面让我们看看 Thread 类中的相关代码:
可以看出每个线程都有 ThreadLocalMap 对象,被命名为 threadLocals
,默认为 null,所以每个线程的 ThreadLocals
都是隔离独享的。
调用 ThreadLocalMap.set()
时,会把当前 threadLocal 对象
作为key
,想要保存的对象作为value
,存入 map。
ThreadLocalMap.set() 的源码如下
/**
* 在 map 中存储键值对<key, value>
*
* @param key threadLocal
* @param value 要设置的 value 值
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 在数组中的下标
int i = key.threadLocalHashCode & (len - 1);
// 遍历一段连续的元素,以查找匹配的 ThreadLocal 对象
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取该哈希值处的ThreadLocal对象
ThreadLocal<?> k = e.get();
// 键值ThreadLocal匹配,直接更改map中的value
if (k == key) {
e.value = value;
return;
}
// 若 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 直到遇见了空槽也没找到匹配的ThreadLocal对象,那么在此空槽处安排ThreadLocal对象和缓存的value
tab[i] = new Entry(key, value);
int sz = ++size;
// 如果没有元素被清理,那么就要检查当前元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
// 扩容的过程也是对所有的 key 重新哈希的过程
rehash();
}
}
Thread、ThreadLocal 以及 ThreadLocalMap 的关系
从上面又可以看出,ThreadLocalMap是在ThreadLocal中使用内部类来编写的,但对象的引用是在Thread中!
于是我们可以总结出:Thread为每个线程维护了ThreadLocalMap这么一个Map(类型是ThreadLocal.ThreadLocalMap,也就是说每个线程有一个自己的ThreadLocalMap ),而ThreadLocalMap保存的Entry
的key
是ThreadLocal对象本身
,value则是要存储的对象
一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!!!
5.remove()
public void remove() {
// 返回当前线程持有的 ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 从 ThreadLocalMap 中清理当前 ThreadLocal 对象关联的键值对
m.remove(this);
}
}
remove 方法的时序图如下所示:
- 根据
当前线程的引用
获取到对应的ThreadLocalMap
- 如果
ThreadLocalMap
不为空,调用它的remove
方法,从 ThreadLocalMap 中清理当前 ThreadLocal 对象关联的键值
六.ThreadLocal 内存泄漏
-
ThreadLocal在ThreadLocalMap
中是以一个弱引用
身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用
来引用它,那么ThreadLocal会在下次GC时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key
的情况,外部读取ThreadLocalMap中的元素是无法通过null Key
来找到Value的。- 弱引用即WeakReference,表示如果弱引用的指向的对象只存在弱引用这一条线路,则下次YGC时会被回收。
- 当仅仅只有ThreadLocalMap中的Entry的key指向ThreadLocal的时候,ThreadLocal会进行回收的!!!
-
因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些
null key
就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value
,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。 -
JVM团队已经考虑到这样的情况,并采取一些措施来保证ThreadLocal尽量不会内存泄漏:在
ThreadLocal的get()、set()、remove()方法
调用的时候会清除掉线程ThreadLocalMap
中所有Entry中Key为null的Value
,并将整个Entry设置为null,利于下次内存回收。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
七.ThreadLocal 应用场景
ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:
- 方便同一个线程使用某一对象,避免不必要的参数传递;
- 线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
- 获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);
- Spring 事务管理器采用了 ThreadLocal
- Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
- 每个线程需要有自己单独的实例
- 实例需要在多个类/方法中共享,但不希望被多线程共享
- 对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
- 对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。扩展:
1)存储用户Session
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
2)解决线程安全的问题
比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String formatDate(Date date) {
return format1.get().format(date);
}
}
这里的DateUtil.formatDate()就是线程安全的了。
- Java8里的 java.time.format.DateTimeFormatter是线程安全的
- Joda time里的DateTimeFormat也是线程安全的
这类场景阿里规范里面也提到了
八.可继承的ThreadLocal-InheritableThreadLocal
ThreadLocal只能用于存储当前线程的变量。子类线程获取不到父类线程的数据。inheritableThreadLocals
就是用来解决父子线程独立变量共享问题。
如果我在主线程中set一个值,这个时候我在新创建的线程中是读取不到的,因为Threadlocal不支持继承性。
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
/**
*测试在主线程中创建子线程,然后获取ThreadLocal的值
*/
@Test
public void testMainCreateChildThread1() {
threadLocal.set(1000);
new Thread(() -> {
System.out.println(Thread.currentThread()+"------"+threadLocal.get());
}).start();
}
输出结果:
Thread[Thread-0,5,main]------null
也就是说Threadlocal不支持继承性
,主线程设置了值,在子线程中是获取不到的
。那我现在想要获取主线程里面的值要怎么做?
Threadlocal有一个子类InheritableThreadLocal 专门用来解决父子线程独立变量共享问题。
static ThreadLocal<Integer> integerInheritableThreadLocal = new InheritableThreadLocal<>();
/**
*测试在主线程中创建子线程,然后获取InheritableThreadLocal的值
*/
@Test
public void testMainCreateChildThread2() {
integerInheritableThreadLocal.set(2000);
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"------"+integerInheritableThreadLocal.get());
}).start();
}
输出结果:
Thread[Thread-0,5,main]====2000
运行结果发现子线程是可以获取到主线程设置的值的,那它是如何实现的?
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);
}
}
InheritableThreadLocal
是继承Threadlocal的,并且把threadlocals
给替换成inheritableThreadLocal么替换成
inheritableThreadLocals`后子线程就可以获取到主线程设置的属性了吗?我们在看一下Thread类中init的的实现。
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//获取主线程
Thread parent = currentThread();
//-------省略无关代码--------------
//-------省略无关代码--------------
/*
*inheritThreadLocals 设置为true并且父类线程inheritableThreadLocals有共享数据则
*创建一个父类线程inheritableThreadLocals副本,然后复制给当前线程的inheritableThreadLocals变量来实现父子线程共享
*/
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//创建一个父类线程inheritableThreadLocals副本并设置到当前线程的inheritableThreadLocals中
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
看Thread
的init()
方法可以看出,先获取了当前线程(主线程)判断当前线程父线程的inheritableThreadLocals
不为空的话就调用ThreadLocal.createInheritedMap
方法赋值给子线程中的inheritableThreadLocals。
但InheritableThreadLocal
仍然有缺陷,一般我们做异步化处理
都是使用的线程池
,而InheritableThreadLocal是在new Thread中的init()
方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。
当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal组件就可以解决这个问题,这里就不再延伸,感兴趣的可自行查阅资料。
九.为什么建议使用static修饰ThreadLocal?
- 首先static修饰的变量是在
类加载
时就分配好内存空间
,在类卸载
才会被回收,这一点请明确.
2.ThreadLocal
是 ThreadLocalMap 中Entry 的 key
,而用 static 修饰 ThreadLocal,保证了 ThreadLocal 有强引用在,也就是 Entry 的 key有被强引用指向,会一直存在,垃圾回收的时候不会被回收 - ThreadLocal的原理是
在Thread内部有一个ThreadLocalMap的集合对象,
他的key是ThreadLocal,value就是你要存储的变量副本, 不同的线程的ThreadLocalMap是相互隔离的,如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源.造成内存泄漏.
十.ThreadLocal的注意事项
1.ThrealLocal脏数据和内存泄漏问题
入坑:
- 脏数据问题:线程复用导致产生脏数据。由于线程池会复用Thread对象,进而Thread对象中的threalLocals也会被复用,导致Thread对象在执行其他任务时通过get()方法获取到之前任务设置的数据,从而产生脏数据。
- 内存泄漏问题:ThreadLocal通常是使用static关键字修饰的。如果开发人员单纯寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的Value,那么就会导致内存泄漏,Entry的Value无法被回收。
脱坑:
- 解决脏数据:线程执行前重新调用set()设置值。线程复用导致产生脏数据,如果复用线程在执行下个任务之前调用set()重新设置值,那么脏数据问题就不会出现了。
- 解决内存泄漏:线程执行完后调用remove()完成收尾工作。无法依托弱引用机制来回收Entry的Value,那就调用ThreadLocal的remove方法显式清除。
最后,Entry的弱引用机制不是导致ThreadLocal内存泄漏的原因,它的存在只是增加了开发人员的理解难度,就算没有弱引用机制,线程执行完不调用remove()清除也会存在内存泄漏问题。
2.ThreadLocal结合线程池的问题
当 ThreadLocal 配合线程池使用的时候,我们需要及时对 ThreadLocal 进行清理,清除与本线程绑定的 value 值,否则会出现意料之外的结果。
来看看没有调用remove方法和有调用remove下的结果差异。
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
executorService.execute(()->{
Integer before = threadLocal.get();
threadLocal.set(before + 1);
Integer after = threadLocal.get();
System.out.println("before: " + before + ",after: " + after);
});
}
executorService.shutdown();
}
没有调用 remove 方法进行清理
before: 0,after: 1
before: 0,after: 1
before: 1,after: 2
before: 2,after: 3
before: 3,after: 4
可以看到出现了 before 不为0的情况,这是因为线程在执行完任务被复用了,被复用的线程使用了上一个线程操作的value对象,从而导致不符合预期。
加上调用remove方法的逻辑:
try {
Integer before = threadLocal.get();
threadLocal.set(before + 1);
Integer after = threadLocal.get();
System.out.println("before: " + before + ",after: " + after);
} finally {
threadLocal.remove();
}
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
3. 线程池异步调用,requestId传递
因为 org.slf4j.MDC
是基于ThreadLocal去实现的,异步过程中,子线程并没有办法获取到父线程ThreadLocal存储的数据,所以这里可以自定义线程池执行器,修改其中的run()方法:
public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> run(runnable, context));
}
@Override
private void run(Runnable runnable, Map<String, String> context) {
if (context != null) {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
MDC.remove();
}
}
}
十一.ThreadLocal原理总结
-
ThreadLocal是用来提供线程局部变量的,在线程内可以随时随地的存取数据,而且线程之间是互不干扰的。
-
ThreadLocal实际上是在每个
线程内部
维护了一个ThreadLocalMap
,这个ThreadLocalMap是每个线程独有的,里面存储的是Entry对象
,Entry对象实际上是个ThreadLocal的实例的弱引用
,同时还保存了value值,也就是说Entry存储的是键值对
,key就是ThreadLocal实例引用,value则是要存储的数据
。 -
TreadLocal的核心是底层维护
的ThreadLocalMap
,它的底层是一个自定义的哈希表
,增长因子是2/3
,增长因子也可以叫做是一个阈值,底层定义为threshold
,当哈希表容量大于或等于阈值的3/4
的时候就开始扩容底层的哈希表数组table
。 -
ThreaLocalMap中存储的核心元素是
Entry
,Entry是一个弱引用
,所以在GC的时候,ThreadLocal如果没有外部的强引用
,它会被回收
掉,这样就会产生key为null的Entry了
,这样也就产生了内存泄漏
。 -
在
ThreadLocal
的get(),
set()
和remove()
的时候都会清除ThreadLocalMap中key为null的Entry
,如果我们不手动清除,就会造成内存泄漏,最佳做法是使用ThreadLocal就像使用锁一样,加锁之后要解锁,也就是用完就使用remove进行清理。