1、基础概念
Stack & Heap
Stack和Heap是我们常说的栈和堆,这里不做赘述,只需要备注一点知识即可,Stack是线程私有独享且线程安全的,Heap是所有线程共享非线程安全的。TheadLocal的设计初衷就是希望让线程拥有了自己内部独享的变量,每个线程之间隔离互不干扰以起到线程安全的目的。
ThreadLocal
- ThreadLocal是我们所说的线程本地变量。如上图所示,它的内部封装了一个非常重要的数据结构ThreadLocalMap来提供线程变量数据的真实获取、存储及移除等操作。我们可以把ThreadLocal理解称为一个封装类或是一个中介对象,所有的核心方法如get、set、remove等都通过ThreadLocal来提供和交互,而真正的幕后大佬是ThreadLocalMap这个封装了最终方法逻辑和内部数据结构的内部类。
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
- ThreadLocal内部还持有一个实例变量threadLocalHashCode,它是一个哈希值用来做ThreadLocalMap中Entry哈希表路由计算的,threadLocalHashCode的生成方式是根据HASH_INCREMENT这个哈希魔数进行自加操作,关于哈希算法这部分后面会提到。
ThreadLocalMap
static class ThreadLocalMap {
// hash map中的entry继承自弱引用WeakReference,指向threadLocal对象
// 对于key为null的entry,说明不再需要访问,会从table表中清理掉
// 这种entry被成为“stale entries”
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
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);
}
}
- ThreadLocalMap是一个自定义的hash map,专门用来保存线程的thread local变量
- 它的操作仅限于ThreadLocal类中,不对外暴露
- 这个类被用在Thread类的私有变量threadLocals和inheritableThreadLocals上
- 为了能够保存大量且存活时间较长的threadLocal实例,hash table entries采用了WeakReferences作为key的类型
- 一旦hash table运行空间不足时,key为null的entry就会被清理掉
Entry
这里的Entry继承了WeakReference类,它的内部构成是一个键值对结构,Key是弱引用的referant,是从WeakReference对象继承而来的,Value是实际存储的线程变量对象数据,即<K,V>=<Referant,Object>,而这里Entry进行了泛型限制,最终定义为Entry<ThrealLocal,Object>的数据格式
2、源码核心方法解析
get()源码
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- 获取当前线程内部的ThreadLocalMap
- map存在则获取当前ThreadLocal对应的value值
- map不存在或者找不到value值,则调用setInitialValue,进行初始化
setInitialValue()源码
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
- 调用initialValue方法,获取初始化值【调用者通过覆盖该方法,设置自己的初始化值】
- 获取当前线程内部的ThreadLocalMap
- map存在则把当前ThreadLocal和value添加到map中
- map不存在则创建一个ThreadLocalMap,保存到当前线程内部
get()方法时序图
set()源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 获取当前线程内部的ThreadLocalMap
- map存在则把当前ThreadLocal和value添加到map中
- map不存在则创建一个ThreadLocalMap,保存到当前线程内部
remove()源码
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove()方法时序图
3、核心算法
哈希值生成算法
哈希值生成算法:HASH_INCREMENT是参与哈希计算的哈希魔数,这里是16进制的0x61c88647,转换为10进制就是-1640531527,
每次自增HASH_INCREMENT进行生成,这里使用了AtomicInteger保证了线程安全
索引生成算法
索引生成算法:key.threadLocalHashCode & (len-1)
- key.threadLocalHashCode即通过HASH_INCREMENT自增1得到的哈希值
- len即当前哈希槽的容量,初始化默认是16,即时哈希槽扩容也是保持16的2倍,可以理解为2的N次方
- 综上,索引生成是(HASH_INCREMENT自增HASH_INCREMENT)&(2的N次方-1)
黄金分割数
ThreadLocalMap采用黄金分割数的方式,大大降低了哈希冲突的情况。
- 黄金分割数: int的黄金分割数是按照int的最大值2147483647乘以黄金分割比例Math.sqrt(5) - 1) / 2 计算得来的,最终int的黄金分割数为-1640531527。
- 哈希魔数: 哈希数是通过哈希魔数HASH_INCREMENT加自身进行变更,经过研究发现这样会具有更好的散列性。
- 高低位: (2的N次方-1)生成的数字均有一个特点,二进制高位为0,低位为1
- 高效按位与: (HASH_INCREMENT自增HASH_INCREMENT)&(2的N次方-1)是索引生成的算法,因为是按位与运算,且(2的N次方-1)高位为0,低位为1,因此变成了HashCode &(2的N次方-1)= HashCode %(2的N次方-1)的运算,但是按位与运算效率要高于取模运算
(2的N次方-1)的二进制
2的1次方 num:1 binary:00000000000000000000000000000001
2的2次方 num:3 binary:00000000000000000000000000000011
2的3次方 num:7 binary:00000000000000000000000000000111
2的4次方 num:15 binary:00000000000000000000000000001111
2的5次方 num:31 binary:00000000000000000000000000011111
2的6次方 num:63 binary:00000000000000000000000000111111
2的7次方 num:127 binary:00000000000000000000000001111111
2的8次方 num:255 binary:00000000000000000000000011111111
2的9次方 num:511 binary:00000000000000000000000111111111
2的10次方 num:1023 binary:00000000000000000000001111111111
2的11次方 num:2047 binary:00000000000000000000011111111111
2的12次方 num:4095 binary:00000000000000000000111111111111
2的13次方 num:8191 binary:00000000000000000001111111111111
2的14次方 num:16383 binary:00000000000000000011111111111111
2的15次方 num:32767 binary:00000000000000000111111111111111
2的16次方 num:65535 binary:00000000000000001111111111111111
2的17次方 num:131071 binary:00000000000000011111111111111111
2的18次方 num:262143 binary:00000000000000111111111111111111
2的19次方 num:524287 binary:00000000000001111111111111111111
2的20次方 num:1048575 binary:00000000000011111111111111111111
2的21次方 num:2097151 binary:00000000000111111111111111111111
2的22次方 num:4194303 binary:00000000001111111111111111111111
2的23次方 num:8388607 binary:00000000011111111111111111111111
2的24次方 num:16777215 binary:00000000111111111111111111111111
2的25次方 num:33554431 binary:00000001111111111111111111111111
2的26次方 num:67108863 binary:00000011111111111111111111111111
2的27次方 num:134217727 binary:00000111111111111111111111111111
2的28次方 num:268435455 binary:00001111111111111111111111111111
2的29次方 num:536870911 binary:00011111111111111111111111111111
2的30次方 num:1073741823 binary:00111111111111111111111111111111
不难发现,2的幂次方-1的数字的二进制有一个特点那就是,高位都是0,,低位都是1。
(HASH_INCREMENT自增HASH_INCREMENT)的二进制
id:1 hashCode:1640531527 binary:01100001110010001000011001000111
id:2 hashCode:-1013904242 binary:11000011100100010000110010001110
id:3 hashCode:626627285 binary:00100101010110011001001011010101
id:4 hashCode:-2027808484 binary:10000111001000100001100100011100
id:5 hashCode:-387276957 binary:11101000111010101001111101100011
id:6 hashCode:1253254570 binary:01001010101100110010010110101010
id:7 hashCode:-1401181199 binary:10101100011110111010101111110001
id:8 hashCode:239350328 binary:00001110010001000011001000111000
id:9 hashCode:1879881855 binary:01110000000011001011100001111111
id:10 hashCode:-774553914 binary:11010001110101010011111011000110
id:11 hashCode:865977613 binary:00110011100111011100010100001101
id:12 hashCode:-1788458156 binary:10010101011001100100101101010100
id:13 hashCode:-147926629 binary:11110111001011101101000110011011
id:14 hashCode:1492604898 binary:01011000111101110101011111100010
id:15 hashCode:-1161830871 binary:10111010101111111101111000101001
id:16 hashCode:478700656 binary:00011100100010000110010001110000
id:17 hashCode:2119232183 binary:01111110010100001110101010110111
id:18 hashCode:-535203586 binary:11100000000110010111000011111110
id:19 hashCode:1105327941 binary:01000001111000011111011101000101
id:20 hashCode:-1549107828 binary:10100011101010100111110110001100
id:21 hashCode:91423699 binary:00000101011100110000001111010011
id:22 hashCode:1731955226 binary:01100111001110111000101000011010
id:23 hashCode:-922480543 binary:11001001000001000001000001100001
id:24 hashCode:718050984 binary:00101010110011001001011010101000
id:25 hashCode:-1936384785 binary:10001100100101010001110011101111
id:26 hashCode:-295853258 binary:11101110010111011010001100110110
id:27 hashCode:1344678269 binary:01010000001001100010100101111101
id:28 hashCode:-1309757500 binary:10110001111011101010111111000100
id:29 hashCode:330774027 binary:00010011101101110011011000001011
id:30 hashCode:1971305554 binary:01110101011111111011110001010010
id:31 hashCode:-683130215 binary:11010111010010000100001010011001
以上通过nextHashCode.getAndAdd(HASH_INCREMENT)生成32次hashCode。
由于(2的N次方-1)的二进制高位均为1,且是&运算,因此hashCode越具有散列性,最终索引值也会具有很好的散列性,哈希碰撞的可能性就会减少。
在(2的N次方-1)的二进制也就是length固定的情况下,低位都是1,高位都是0,因此,hashCode高位或低位相同太多会导致严重碰撞,一定要如上图这样到高低位都能具有很好的差异性参与计算才可以减少碰撞
id:1 hashCode:1640531527 index:7
id:2 hashCode:-1013904242 index:14
id:3 hashCode:626627285 index:21
id:4 hashCode:-2027808484 index:28
id:5 hashCode:-387276957 index:3
id:6 hashCode:1253254570 index:10
id:7 hashCode:-1401181199 index:17
id:8 hashCode:239350328 index:24
id:9 hashCode:1879881855 index:31
id:10 hashCode:-774553914 index:6
id:11 hashCode:865977613 index:13
id:12 hashCode:-1788458156 index:20
id:13 hashCode:-147926629 index:27
id:14 hashCode:1492604898 index:2
id:15 hashCode:-1161830871 index:9
id:16 hashCode:478700656 index:16
id:17 hashCode:2119232183 index:23
id:18 hashCode:-535203586 index:30
id:19 hashCode:1105327941 index:5
id:20 hashCode:-1549107828 index:12
id:21 hashCode:91423699 index:19
id:22 hashCode:1731955226 index:26
id:23 hashCode:-922480543 index:1
id:24 hashCode:718050984 index:8
id:25 hashCode:-1936384785 index:15
id:26 hashCode:-295853258 index:22
id:27 hashCode:1344678269 index:29
id:28 hashCode:-1309757500 index:4
id:29 hashCode:330774027 index:11
id:30 hashCode:1971305554 index:18
id:31 hashCode:-683130215 index:25
id:32 hashCode:957401312 index:0
以上是length为32时,生成的32次索引值的情况,发现索引值分布非常均匀,没有出现碰撞。
哈希冲突解决
当出现哈希冲突时,它的做法看是否是同一个对象或者是是否可以替换,否则往后移动一位,继续判断,这里采用的是再次寻址的方法。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
内存清理
ThreadLocal之所以采用Entry的key使用弱引用就是为了尽快回收避免大量占用内存空间,除此之外还创造性的增加了探测式清理、启发式清理两种方式在核心方法get、set等调用时进行内存对象的回收和清理工作
探测式清理
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
tab[staleSlot].value = null;
// 显式设置该entry为null,以便垃圾回收
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 清理对应ThreadLocal已经被回收的entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
/*
* 对于还没有被回收的情况,需要做一次rehash。
*
* 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
* 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
*/
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
/*
* 在原代码的这里有句注释值得一提,原注释如下:
*
* Unlike Knuth 6.4 Algorithm R, we must scan until
* null because multiple entries could have been stale.
*
* 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列)
* 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。
* R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引
* 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index,
* 继续向后扫描直到遇到空的entry。
*
* ThreadLocalMap因为使用了弱引用,所以其实每个slot的状态有三种也即
* 有效(value未回收),无效(value已回收),空(entry==null)。
* 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。
*
* 因为expungeStaleEntry函数在扫描过程中还会对无效slot清理将之转为空slot,
* 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。
*/
while (tab[h] != null) {
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
// 返回staleSlot之后第一个空的slot索引
return i;
}
- 根据场景改进了高德纳论述的从使用线性探测的散列表中删除一个元素的R算法
- 如果index对应的slot就是要读的threadLocal,则直接返回结果
- 调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
- 没有找到key,返回null
启发式清理
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
// 扩大扫描控制因子
n = len;
removed = true;
// 清理一个连续段
i = expungeStaleEntry(i);
}
} while ((n >>>= 1) != 0);
return removed;
}
- 启发式地清理slot,i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空),n是用于控制控制扫描次数的
- 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
- 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理,再从下一个空的slot开始继续扫描
- 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,区别是前者传入的n为元素个数,后者为table的容量
4、缺点
内存泄露问题
如上图,整理对象引用关系,用**++> 表示强引用,用 –>**表示弱引用
- Thread ++> ThrealLocal.ThreadLocalMap ++> Entry ++> key (referant) --> ThreadLocal
- Thread ++> ThrealLocal.ThreadLocalMap ++> Entry ++> value ++> Object
由于key (referant) --> ThreadLocal是弱引用,gc时会回收key,此时key为null,但是Thread作为这条引用链的Root根不会立刻线程执行完毕而消失,会一直驻留在Stack中,这种情况一般可以有两种,一个事在一个for循环中执行,一种是线程池中执行,因此会导致强引用的value不会进行释放对象导致内存溢出
父子线程无法传递线程副本数据
在主线程中使用ThreadLocal无法直接传递给子线程,如果要操作还需要通过线程封闭进行变量置换。
5、问题汇总
- ThreadLocal的key是弱引用,那么在 threadLocal.get()的时候,发生GC之后,key是否为null?
由于Entry<WeakReference,Object>的key是弱引用,GC后就会被回收 - ThreadLocal中ThreadLocalMap的数据结构?
哈希表 - ThreadLocalMap的Hash算法?
key.threadLocalHashCode & (length -1) , length为2的幂次方 - ThreadLocalMap中Hash冲突如何解决?
开放地址,二次寻址,由于使用黄金分割数进行哈希计算,散列非常好,出现碰撞的可能性很低,所以没有像HashMap那样进行链地址解决冲突 - ThreadLocalMap扩容机制?
length2/3 触发rehash逻辑,进行探测式清理,最终判断size >= threshold 3/4来决定是否要真正扩容调用resize方法 - ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程?
探测式清理(expungeStaleEntry())、启发式清理(cleanSomeSlots())
探测式清理是以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理,结合了 - ThreadLocalMap.set()方法实现原理?
略 - ThreadLocalMap.get()方法实现原理?
略 - 项目中ThreadLocal使用情况?遇到的坑?
父子线程不能传递线程变量,主线程中使用线程池相当于父线程中使用子线程无法传值
6、实战应用
复杂场景
Spring容器、RPC全链路traceId传递等
应用举例
在单体应用中一般不会声明多个ThreadLocal,即不会让Thread中持有的ThreadLocalMap的key有多个,由于ThreadLocal是支持泛型的,我们可以传入一个线程安全的容器,让Thread内持有的ThreadLocalMap中只有一个key,即只有一个ThreadLocal的key引用,而存储的Object可以是一个Map或者List,我们根据业务场景操作容器即可,大部分情况都可以满足,设计合理的话是可以共用的,减少持有key也不会占用大量的栈空间,且把ThreadLocal声明为private final static,下面提供一个demo:
/**
* @author: guanjian
* @date: 2020/07/08 9:31
* @description: 环境变量
*/
@Component("contextHolder")
public class ContextHolder<T, R> {
private final static Logger LOGGER = LoggerFactory.getLogger(ContextHolder.class);
/**
* 入参对象
*/
public final static String REQUEST_PARAM = "request_param";
/**
* 出参对象
*/
public final static String RESPONSE_PARAM = "response_param";
/**
* 传值对象
*/
public final static String TRANSMIT_PARAM = "transmit_param";
/**
* 线程变量
*/
private final static ThreadLocal<Map<Object, Object>> localVariable = ThreadLocal.withInitial(() -> Maps.newHashMap());
public void bindLocal(Object key, Object value) {
Objects.requireNonNull(key, "key can not be null");
Map holder = localVariable.get();
holder.put(key, value);
localVariable.set(holder);
LOGGER.debug("[ContextHolder] key={},value={} binded.", key, JSON.toJSONString(value));
}
public Object getLocal(Object key) {
if (CollectionUtils.isEmpty(localVariable.get())) return null;
Object value = localVariable.get().get(key);
LOGGER.debug("[ContextHolder] key={},value={} getted.", key, JSON.toJSONString(value));
return value;
}
public void bindRequest(T value) {
bindLocal(REQUEST_PARAM, value);
}
public T getRequest() {
return (T) localVariable.get().get(REQUEST_PARAM);
}
public void bindResponse(R value) {
bindLocal(RESPONSE_PARAM, value);
}
public R getResponse() {
return (R) localVariable.get().get(RESPONSE_PARAM);
}
public void bindTransmit(Object value) {
bindLocal(TRANSMIT_PARAM, value);
}
public Object getTransmit() {
return getLocal(TRANSMIT_PARAM);
}
public void clear() {
localVariable.remove();
}
}
7、参考
https://www.cnblogs.com/wang-meng/p/12856648.html
https://blog.csdn.net/zjcsuct/article/details/104310194
http://www.iocoder.cn/JDK/ThreadLocal/
https://blog.csdn.net/qq_22167989/article/details/89448670