弱小和无知并不是生存的障碍,傲慢才是。
---- ---- 面试者
总结
ThreadLocal是一个线程级别的缓存,作用域只在当前线程,跨线程访问是无效的,因为数据是存储在Thread对象的threadLocalMap属性中,其中threadLocalMap是一个Entry数组,Entry对象的key是声明的threadLocal变量,值是threadLocal变量set的值。由于Entry的key在定义时是申明的弱引用,故当threadLocal变量无强引用之后,在发生GC时,这个变量会被回收掉,即threadLocalMap中会存在部分桶位的Entry对象的key是null,这样在使用ThreadLocal时需要手动进行remove,避免出现内存泄漏。
基本属性
由于ThreadLocal本质上是存储在ThreadLocalMap中,而ThreadLocalMap底层是一个数组,那么就涉及到hash选桶,以及进行扩容。
// threadLocal对象的下一个hashcode = nextHashCode + HASH_INCREMENT
private final int threadLocalHashCode = nextHashCode();
// threadLocal对象hashcode初始值,从0开始
private static AtomicInteger nextHashCode = new AtomicInteger();
// threadLocal对象的hashcode偏移量,这玩意近似1,643,527,751≈2次方32 × φ(φ=0.618...) ,φ:黄精比例
private static final int HASH_INCREMENT = 0x61c88647;
// 空的构造函数
public ThreadLocal() {
}
// 实际存储数据的Map
static class ThreadLocalMap {
// 初始容量
private static final int INITIAL_CAPACITY = 16;
// 存储treadLocal数据的数组
private Entry[] table;
// 初始大小
private int size = 0;
// 扩容阈值,table.length * 2/3
private int threshold;
// 存储threadLocal数据的数据结构
static class Entry extends WeakReference<ThreadLocal>> {
// 存储threadLocal设置的value
Object value;
}
}
Java的引用
- 强引用:平时我们对象的创建和变量的申明都是强引用,根据垃圾回收算法进行回收;
- 软引用:使用SoftReference类来表示,在内存不足时,JVM才会进行回收;
- 弱引用:使用WeakReference类来表示,当发生GC时就会被回收;
- 虚引用:不影响对象的生命周期,使用PhantomReference类表示,任何时候都可能被回收;
set方法流程
由于ThreadLocal声明的变量,本质上是存储在当前线程Thread对象的threadLocalMap属性中,而threadLocalMap具有一定的Map特性,故在set值时,会和Map有些相似;
- 现根据当前线程获取localThreadMap属性;
- 如果localThreadMap为空,则先进行初始化,默认length为16,扩容阈值为16*2/3
- 如果localThreadMap不为空则进行set处理,首先进行hash计算与选择存储数据的槽位;
- 如果选择的槽位不为空,且key与当前set的key一致,则进行值覆盖;
- 如果槽位有值,且与set的key不一样,则从当前槽位往后遍历,直到找到相同key进行覆盖,或者找到第一个为null的槽位,进行初始化赋值;
- 如果这期间发现存在槽位不为null,但是key为null的,则找到最小槽位开始往后遍历进行数据清理,并且把后边正常的数据往前迁移,直到碰到为null的槽位(因为localThreadMap的数组初始化之后,set值时是根据hash进行选位,故数据是散列存入,不连续的);
- 存入数据之后,如果达到扩容阈值,则进行扩容,扩容到原有的两倍,然后把旧值赋值过去即可;
// set数据方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap ,这个对象定义在ThreadLocal,但是使用申明在Thread类
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果当前map不为空
map.set(this, value);
else
// 如果为空,则创建
createMap(t, value);
}
private void set(ThreadLocal> key, Object value) {
// table是存储数据的数组
Entry[] tab = table;
int len = tab.length;
// 进行hash计算,通过 & (len - 1)进行index
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 这里选择index之后,可能存在hash冲突,
// 由于threadlocalMap只是数组,无链表结构,故hash冲突了,直接后移
ThreadLocal> k = e.get();
if (k == key) {
// 当前index里存储的key与存入的key一样,则对value进行覆盖
e.value = value;
return;
}
if (k == null) {
// 如果key = null ,则说明当前index存在过期的数据,需要进行清理
replaceStaleEntry(key, value, i);
return;
}
}
// 上侧代码知道找到一个为null的地方,然后把数据存储下来
tab[i] = new Entry(key, value);
// 容量 + 1
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 替换过期的数据
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
// 当前threadLocal数据key为null,需要清理
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
// 从当前staleSlot,不停的向前找 i - 1,如果到头了,就跳转到尾部继续向前,
// 尾部一般为null,就跳出循环了
if (e.get() == null)
// 找到index最小的,需要清理的数据index
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 向后寻找不为null的数据
ThreadLocal> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
// 如果不为null的数据key与存储的key一样,则对值进行覆盖
e.value = value;
// 把位于staleSlot的过期数据,置换到i所在的地方
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists 开始删除过期数据
if (slotToExpunge == staleSlot)
// 这里因为staleSlot已经被替换到i了,故slotToExpunge设置成i
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
// 如果在k == key之前,找到从staleSlot开始往后最小的slotToExpunge
slotToExpunge = i;
}
// 如果不存在key,则staleSlot设置null进入gc,在针对staleSlot位进行赋值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
// 这里说明除了staleSlot外,还存在过期数据,在上边判断key是否存在时,会校验并对slotToExpunge修改
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
// 把staleSlot位置以及以后的过期数据设置为null,并且把后边正常的数据,进行前移,保证table数据的连续性
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
// 判定staleSlot后边不为null的数据
if (k == null) {
// 如果key引用已经为null,则把value也设置为null
e.value = null;
// 同时把i锁在的槽设置为null
tab[i] = null;
size--;
} else {
// 如果staleSlot后边的数据是正常的
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
// 这里h != i ,说明当前e是从h槽位不停地后移到i的
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
// 找到从h开始,第一个为null的槽位,把e进行前移
h = nextIndex(h, len);
// e进行前移
tab[h] = e;
}
}
}
// 返回整理过程中,碰到的第一个为null的槽位
return i;
}
// 最后,再进行一次清理,i开始清理的地方,n数组长度或size
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// 获取下一个元素槽位
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
// 如果当前槽位数据已过时
i = expungeStaleEntry(i);
}
// 只处理 n对2的解幂次
} while ( (n >>>= 1) != 0);
return removed;
}
// 扩容,因为存储是数组,故涉及库容
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 扩容到原数组的2倍
int newLen = oldLen * 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();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
// 如果槽位冲突,则往后顺位+1
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 设置扩容阈值 2/3倍
setThreshold(newLen);
size = count;
table = newTab;
}
get方法流程
由于threadLocalMap是一个数组,故现根据hash进行选择槽位,然后从当前槽位开始遍历对比key值,直到找到相等的返回,找不到,则返回null。
// 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;
}
}
// 进行threadLocalMap初始化,并把当前线程对应ThreadLocal作为key的value = null
return setInitialValue();
}
remove方法流程
remove方法要现根据key进行hash选择槽位,然后遍历对比key值,找到相等的之后,把key设置成null,value不处理,然后调用过期数据清理方法,对整个数组进行遍历清理。
// 移除值
private void remove(ThreadLocal> key) {
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)]) {
if (e.get() == key) {
// 先根据槽位,然后开始遍历,判定key相等,则移除
// 这里只是把key设置成null了,但是value并未处理
e.clear();
// 移除之后,开始清理垃圾数据
expungeStaleEntry(i);
return;
}
}
}
内存泄露
由于ThreadLocalMap中的key是一个若引用,当针对ThreadLocal的引用被销毁,但是线程还存活的时候(线程池使用场景),如果针对ThreadLocal的使用,最后没有进行手动remove,则当放生GC之后,由于ThreadLocal都被回收,故会存在大量的ThreadLocalMap的槽位其key为null,而value不为null,导致内存泄露。不过ThreadLocal在set、remove等方法做了垃圾数据回收,但是前提是得重新触发set或是remove,故为了保证内存不泄露,一定要在ThreadLocal生命周期结束时的finally方法里,进行remove处理。