ThreadLoal源码解析
关于弱引用和内存泄漏、hash冲突的解决内容,
来源于:【黑马程序员Java基础教程由浅入深全面解析threadlocal-哔哩哔哩】 https://b23.tv/Q6if7Ga
Thread类源码
/*
* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
* 与此线程相关的ThreadLocal值。此映射由ThreadLocal类维护。
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal类源码
/**
* ThreadLocals依赖于附加到每个线程的每个线程线性探测哈希映射(thread.ThreadLocals和可继承的ThreadLocal)。
* ThreadLocal对象充当键,通过threadLocalHashCode进行搜索。
* 这是一个自定义哈希代码(仅在ThreadLocalMaps中有用),在相同线程使用连续构造的ThreadLocal的常见情况下消除了冲突,
* 而在不常见的情况下保持良好的行为。
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 要给出的下一个哈希代码。原子更新。从零开始。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* 连续生成的哈希码之间的差异-将隐式顺序线程本地ID转换为两个大小表的幂的近似最优的乘法哈希值。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一个哈希代码。
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 设置调整大小阈值,在最坏的情况下为 2/3 负载系数
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 根据传入的下标,返回下一个下标 (环形: 0-1-...-(len-1)-len-0-1-...-len)
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* ThreadLocalMap 内部类
*/
static class ThreadLocalMap {
private Entry[] table;//数据数组
private int size = 0;//数组大小
private int threshold; //阈值
private static final int INITIAL_CAPACITY = 16; //默认大小
/*
* Entry 继承WeakReference,并且用ThreadLocal作为key.
* 如果key为null(entry.get() == null),意味着key不再被引用,
* 因此这时候entry也可以从table中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; //存储线程值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始化
*/
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);
}
}
弱引用和内存泄漏:
有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。
我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。
(1) 内存泄漏相关概念
- Memory overflow:内存溢出,没有足够的内存提供申请者使用。
- Memory leak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出
(2) 弱引用相关概念
Java中的引用有4种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
**强引用(“Strong” Reference)**,就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
**弱引用(WeakReference)**,垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
(3) 如果key使用强引用
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用)如下:
假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
(5)如果key使用弱引用
那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
(6)出现内存泄漏的真实原因
比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?
细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:
1. 没有手动删除这个Entry
2. CurrentThread依然运行
第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
第二点稍微复杂一点, 由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。
综上,**ThreadLocal内存泄漏的根源是**:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
(7) 为什么使用弱引用
根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
使用完ThreadLocal,调用其remove方法删除对应的Entry
使用完ThreadLocal,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,**弱引用比强引用可以多一层保障**:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
hash冲突的解决
hash冲突的解决是Map中的一个重要内容。我们以hash冲突的解决为线索,来研究一下ThreadLocalMap的核心源码。
(1) 首先从ThreadLocal的set() 方法入手
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
//调用了ThreadLocalMap的set方法
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//调用了ThreadLocalMap的构造方法
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
这个方法我们刚才分析过, 其作用是设置当前线程绑定的局部变量 :
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
**(这里调用了ThreadLocalMap的set方法)**
C. 如果Map为空,则给该线程创建 Map,并设置初始值
**(这里调用了ThreadLocalMap的构造方法)**
这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析这两个方法。
(2)构造方法`ThreadLocalMap(ThreadLocal<?> firstKey, Object 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);
}
构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。
**重点分析**: `int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)`。
a. 关于firstKey.threadLocalHashCode
:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
private static AtomicInteger nextHashCode = new AtomicInteger();
//特殊的hash值
private static final int HASH_INCREMENT = 0x61c88647;
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,`HASH_INCREMENT = 0x61c88647`,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。
b. 关于& (INITIAL_CAPACITY - 1)
计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
(3) ThreadLocalMap中的set方法
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,说明之前的 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关联的对象已经被回收,所以这个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);
}
代码执行流程:
A. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,
B. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
C. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
D. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
重点分析 : ThreadLocalMap使用线性探测法
来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述,可以把Entry[] table看成一个环形数组。