1. 介绍ThreadLocal
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。
总结:就是它提供线程内的局部变量,不同的线程之间不会相互干扰(线程隔离),这种变量在线程的生命周期内起作用,减少同一个线程内多个函数之间一些公共变量传递的复杂度。
2. Thread,ThreadLocal,ThreadLocalMap这三个东西到底是什么
下面一张图描述了它们之间的关系:
具体的关系是这样的:
- 每个Thread线程内部都有一个ThreadLocalMap
- ThreadLocalMap里面存储ThreadLocal对象(key)和线程设置的变量(value)
- ThreadLocalMap是由Thread维护的,由ThreadLocal负责向ThreadLocalMap获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
3. ThreadLocal里的核心方法解析
方法声明 | 描述 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
以下是这3个方法的详细源码分析(为了保证思路清晰, ThreadLocalMap部分这里暂时不展开,在后面详细讲解)
3.1 set方法解析
/**
* 设置当前线程与ThreadLocal对应的value的值
*
* @param value 将要保存在当前线程与ThreadLocal对应的value的值
*/
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 {
// 即当前线程不存在ThreadLocalMap对象
// 调用createMap方法创建Map对象
// 并将 t(当前线程)和value作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @return 当前线程对应的ThreadLocalMap对象
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建当前线程Thread对应的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代码执行流程:
- 首先获取当前线程,并根据当前线程获取ThreadLocalMap
- 如果获取的ThreadLocalMap不为空,则将参数设置到ThreadLocalMap中 (以当前ThreadLocal的引用作用key)
- 如果ThreadLocalMap为空,则给该线程创建ThreadLocalMap,并设置值
3.2 get方法解析
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用 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 {
// 不存在就创建一个ThreadLocalMap,将ThreadLocal和value设置进去
createMap(t, value);
}
// 如果当前对象是TerminatingThreadLocal类的实例
if (this instanceof TerminatingThreadLocal) {
// 就将该对象注册到TerminatingThreadLocal类中
// 作用是在线程终止时自动执行一些清理操作,例如释放资源、关闭连接等。
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
/**
* 返回当前线程对应的ThreadLocal的初始值
*
* 这个方法就是简单的返回null
* 如果想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承 ThreadLocal 的方式去重写此方法
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
代码执行流程
- 首先获取当前线程,根据当前线程获取ThreadLocalMap
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry,否则跳转到4
- 如果获取的Entry不为空,就返回Entry.value,否则跳转到4
- 如果Map为空或Entry为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map,最后将初始值value返回
总结:先获取当前线程的ThreadLocalMap,如果存在就返回ThreadLocal对应存储的值,不存在就创建一个Map并返回初始值
3.3 remove方法解析
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null) {
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
}
代码执行流程
- 首先获取当前线程,并获取当前线程维护的ThreadLocalMap对象
- 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
4. ThreadLocalMap的源码分析
在分析ThreadLocal方法的时候,ThreadLocal的操作实际上是围绕ThreadLocalMap展开的。ThreadLocalMap的源码相对比较复杂, 我们从以下三个方面进行讨论。
4.1 基本结构
(1) 成员变量
/**
* 初始容量 —— 必须是2的整次幂(后面会解释为什么一定要2的整次幂)
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的整次幂。
*/
private Entry[] table;
/**
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
*/
private int size = 0;
/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0
(2) 存储结构 - Entry
/*
* Entry继承WeakReference,并且用ThreadLocal作为key.
* 如果key为null(e.refersTo(null)),意味着key不再被引用,
* 因此这时候entry也可以从table中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
4.2 弱引用和内存泄漏
大家可能在使用ThreadLocal的时候,听到最多的话可能就是:ThreadLocal使用不当的话会导致内存泄漏,那接下来就深扒一下ThreadLocal的内存泄漏。
在分析之前先解释一下提到的几个概念:
(1)内存泄漏
内存泄漏:程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
内存溢出:没有足够的内存提供申请者使用,导致程序终止。
(2)弱引用
Java中的引用有4种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
(3)过程分析
了解完相关概念后,我们先来看看ThreadLocal的内存图
我们假设在业务代码中使用完ThreadLocal ,ThreadLocalRef被回收了。
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向ThreadLocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
但是在没有手动删除这个Entry以及当前Thread依然运行的前提下,也存在有强引用链 ThreadRef->Thread->ThreadLocalMap->Entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用, 确实导致了内存泄漏。
但是不是只要使用强引用,就能避免内存泄漏了呢,我们继续往下看
假设在业务代码中使用完ThreadLocal ,ThreadLocalRef被回收了。
但是因为ThreadLocalMap的Entry强引用了ThreadLocal,造成ThreadLocal无法被回收。
在没有手动删除这个Entry以及当前Thread依然运行的前提下,始终有强引用链 ThreadRef->Thread->ThreadLocalMap->Entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用,也是完全无法避免内存泄漏的,反而可能会加剧内存泄漏的问题。
(4)解决方法
根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
-
使用完ThreadLocal,调用其remove方法删除对应的Entry,让ThreadLocalMap->Entry中间的引用断开,让垃圾回收器回收
-
使用完ThreadLocal,将当前Thread也随之运行结束,让ThreadRef->Thread中间的引用断开,让垃圾回收器回收
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。(后面会详细解析ThreadLocalMap中的方法)
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
4.3 ThreadLocalMap中的核心方法
方法声明 | 描述 |
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) | 构造方法 |
private void set(ThreadLocal<?> key, Object value) | set方法 |
private Entry getEntry(ThreadLocal<?> key) | 获取Entry |
private void remove(ThreadLocal<?> key) | 移除Entry |
4.3.1 构造方法
/*
* firstKey : 本ThreadLocal实例(this)
* firstValue : 要保存的线程本地变量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table
table = new Entry[INITIAL_CAPACITY];
// 计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 设置值
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置阈值,后续的方法中会使用到这个阈值
setThreshold(INITIAL_CAPACITY);
}
/**
* 设置阈值,默认为长度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
执行流程:构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。
重点分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
(1) firstKey.threadLocalHashCode
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647
,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。
(2)关于&(INITIAL_CAPACITY - 1)
计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
当size是2的幂次方时(例如:1,2,4,8,16),size - 1 的二进制表示形式将会是低位全为1,而高位为0,例如如果size = 8(二进制为1000),那么size - 1 等于7(二进制为01111)。
此时,当你用一个数(哈希码)与size - 1 进行按位与操作(&),实际上是保留了该位的低位值,去除了高位值。这相当于取得了该数在模size下的某些特定余数。
为什么要这么做?因为对于2的幂次方大小的哈希表,使用 &
运算可以比使用 %
运算更高效。位运算比模运算快,因为它避免了除法的计算过程。此外,这种方法也能确保得到的索引值总是在合法的哈希表索引范围内。
总结:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 一句话就是计算该实体在table中的存放位置的,并采用了一些手段尽可能避免hash冲突。
4.3.2 set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//计算索引(刚才分析过了)
int i = key.threadLocalHashCode & (len-1);
/**
* 使用线性探测法查找元素(重点代码)
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// refersTo方法在这里的作用
// 判断当前的弱引用是否指向了当前传进来的ThreadLocal对象。
if (e.refersTo(key)) {
// 是的话就设置值
e.value = value;
return;
}
// 表示key为null,但是值不为null,说明之前的ThreadLocal对象已经被回收了
// 当前数组中的Entry是一个陈旧(stale)的元素
if (e.refersTo(null)) {
// 用新元素代替陈旧的元素,这个方法进行了不少的垃圾清理,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots用于清除那些ThreadLocal已经被回收的元素,
* 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
* 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行
* rehash(执行一次全表的扫描清理工作)
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
代码执行流程:
- 首先根据key计算出索引i,然后查找i位置上的Entry
- 若是Entry已经存在并且当前Entry指向传入的ThreadLocal (key),那么这时候直接给这个Entry赋新的value值
- 若是Entry存在,但是当前Entry指向null (ThreadLocal被回收了),则调用replaceStaleEntry来更换这个key为空的Entry
- 不断循环检测,直到遇到为null的地方,这时候要是没在循环过程中return,那么就在这个null的位置创建一个Entry,并且插入,同时size加1
- 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>=threshold (负载因子),满足条件的话就会调用rehash函数执行一次全表的扫描清理
重点分析: ThreadLocalMap使用线性探测法来解决哈希冲突的。
来看看其中的nextIndex方法
/**
* 将传进来的index + 1 ,如果比len小,就返回,比len大,就返回0
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
所以,可以把Entry[] table看成一个环形数组来看。
4.3.3 getEntry方法
/**
* 获取与key相关联的Entry对象
* @param ThreadLocal对象
* @return 与key关联的Entry,如果没有,则为null
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* 当在其哈希槽中找不到对应的key时,调用该方法,寻找Entry
*
* @param key ThreadLocal对象
* @param i 哈希索引
* @param e entry对象
* @return 与key关联的条目,如果没有,则为null
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
// 找到了,就返回
if (e.refersTo(key))
return e;
// 如果key为null,就清理掉
if (e.refersTo(null))
// 清理操作
expungeStaleEntry(i);
else
// 下一个位置
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
4.3.4 remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算索引
int i = key.threadLocalHashCode & (len-1);
// 循环着找,指向key的Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.refersTo(key)) {
// 清除与key关联的值
e.clear();
// 将table数组中与key关联的条目从表中删除
expungeStaleEntry(i);
return;
}
}
}
在ThreadLocalMap中,每个Entry对象都包含一个指向ThreadLocal对象的弱引用和一个指向实际存储的数据的强引用。当调用remove方法时,需要同时清除这两个引用,以确保对应的数据可以被垃圾回收器回收,并且不会因为ThreadLocal对象被回收而导致内存泄漏。
呼~累死了,如果有错还希望大家指正,希望能帮助到大家。