ThreadLocal源码分析
结构分析
ThreadLocal叫做本地线本地线程,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
public class ThreadLocal{
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
// 获取(当前)线程t内部的ThreadLocaMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//创建的新线程第一次进来时,threadLocals=null,现在开始创建并赋值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
private int threshold; // Default to 0
// 扩容阈值,size达到数组长度的2/3就扩容
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private Entry[] table;
//使用弱引用,弱引用一旦被GC发现就会回收,防止内存泄漏
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];//默认大小16
//根据传入的localThread得到其对于的数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//计算数组扩容的阈值,这不是我们关注的重点
setThreshold(INITIAL_CAPACITY);
}
private Entry getEntry(ThreadLocal<?> key) {
//根据传入的localThread得到其对于的数组下标
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);
}
}
}
每个线程Thread都有一个自己的ThreadLocalMap
在每个线程创建的时候,并不会为threadLocals赋值。
public class Thread implements Runnable {
// 用于 ThreadLocal
ThreadLocal.ThreadLocalMap threadLocals = null;
// 用于 InheritableThreadLocal (InheritableThreadLocal extends ThreadLocal)
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//这个currentThread()方法是一个被native修饰的本地方法
public static native Thread currentThread();
}
只有当第一次调用ThreadLocal的set(obj)方法,再调用getMap(t)时,发现为null,再调用createMap()完成创建并赋值。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//创建的新线程第一次进来时,threadLocals=null,现在开始创建并赋值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这个时候我们发现,实际上存储的是ThreadLocalMap对象里面的一个数组Entry[] table中的一个元素entry,继承自WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
//将传入的ThealLocal使用弱引用,弱引用一旦被GC发现就会回收,防止内存泄漏
super(k);
value = v;
}
}
ThreadLocalMap
开放寻址法解决hash冲突
使用线性探测法,f(i)=i+1,对i进行递增。
元素插入
开放寻址法的核心是如果出现了散列冲突,就重新探测一个空闲位置,将其插入。当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
元素查找
在散列表中查找元素的过程有点儿类似插入过程。我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。
元素删除
ThreadLocalMap跟数组一样,不仅支持插入、查找操作,还支持删除操作。对于使用线性探测法解决冲突的散列表,删除操作稍微有些特别。我们不能单纯地把要删除的元素设置为空。
还记得我们刚讲的查找操作吗?在查找的时候,一旦我们通过线性探测方法,找到一个空闲位置,我们就可以认定散列表中不存在这个数据。但是,如果这个空闲位置是我们后来删除的,就会导致原来的查找算法失效。本来存在的数据,会被认定为不存在。这个问题如何解决呢?
-
我们可以在删除元素之后,将之后不为null的数据rehash,这样就不会影响查询的逻辑。
-
也可以将删除的元素,特殊标记为 deleted 。当线性探测查找的时候,遇到标记为 deleted 的空间,并不是停下来,而是继续往下探测
装载因子
你可能已经发现了,线性探测法其实存在很大问题。当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。
极端情况下,我们可能需要探测整个散列表,所以最坏情况下的时间复杂度为 O(n) 。同理,在删除和查找时,也有可能会线性探测整张散列表,才能找到要查找或者删除的数据。
不管采用哪种探测方法,hash函数设计得在合理,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子(load factor)来表示空位的多少。
装载因子的计算公式是:散列表的装载因子=填入表中的元素个数/散列表的长度 装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
ThreadLocalMap里面的table,在size达到数组长度的2/3就扩容,两倍扩容,默认table长度16。
使用线性探测法(开放寻址法的一种)解决hash冲突。
static class ThreadLocalMap {
// table默认初始容量:16
private static final int INITIAL_CAPACITY = 16;
// entry数组,用于存储数据
private Entry[] table;
// table已存放元素个数
private int size = 0;
// 扩容阈值,size>threshold就进行rehash(),如果清理完后size>threshold*0.75就要扩容resize()
private int threshold;
// 设置扩容阈值,负载因子:2/3,size达到table.length的2/3就要调用rehash()
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 解决hash冲突:开放寻址法--》线性探测法
// 传入当前索引i,table长度len,计算下一次寻址索引下标,每次索引+1,超出table长度则从0开始。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
}
获取元素
从getEntry()开始
private Entry getEntry(ThreadLocal<?> key) {
// 这个都是老朋友了,根据hash值计算[0,table.length)范围内的索引下标。
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);
}
getEntryAfterMiss()
使用线性探测法获取因为hash冲突而变更位置的元素。
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;
// 目标位置的ThrealLocal被垃圾回收器回收掉了,进行清除脏数据,这是一个关键的、重要的方法
if (k == null)
expungeStaleEntry(i);
// 获取下一个索引下标,就类似于HashMap使用链表法解决hash冲突,对链表进行遍历,获取该冲突桶位的下一个节点
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
expungeStaleEntry() 归位
该方法实际上做了2件事:
-
清除脏数据(key被回收,但是Entry不为null)
-
复位: 将原本不在此处的元素尽量向实际存储的位置靠近。从当前位置开始遍历途径的所有元素,直到遇到null为止,将遍历的每个元素k通过类似于set()的方法进行迁移到新位置,以便于之后get的时候能都找到。
无论是replaceStaleEntry()方法还是cleanSomeSlots()方法还是refresh()方法,最终调用的是expungeStaleEntry()
,你可以在ThreadLocalMap中的get,set,remove都能发现调用它的身影。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 在过时插槽处清除脏数据Entry,并对size进行递减
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 重新散列,直到我们遇到null
Entry e;
int i;
// nextIndex():使用线性探测法获取下一个索引下标
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;//直到我们遇到null就终止循环
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 说明该位置的ThreadLocal被回收了,是脏数据,在此清除掉
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 正常数据,当前key的hash值计算出 预期存储的index为h,但实际存储下标为i,对于发生哈希冲突从而往后寻找空闲位置的的元素可能发生这种情况,这时候需要进行移动,从将它放在新的位置,以便后续能寻找到。
// 如果两者相等就无需操作,跳过这个节点,检查下一个节点
int h = k.threadLocalHashCode & (len - 1);
// 预期下标h与实际下标i不等,将e移动到正确的位置,也就是h所在的位置
// 如果h下标有元素了,使用线性探测法不断往后面寻找,直到找到第一个空闲位置(tab[h]==null),然后tab[h]=e。
if (h != i) {
tab[i] = null;
// 线性探测法寻找空闲位置
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
新增元素
有了前面的基础,在看set方法就简单多了。
-
采用线性探测法,寻找合适的插入位置。首先判断key是否存在,存在则直接覆盖。如果key不存在证明被垃圾回收了此时需要用新的元素替换旧的元素
-
不存在对应的元素,需要创建一个新的元素
-
清除entry不为空,但是ThreadLocal(entry的key被回收了)的元素,防止内存泄露
-
如果满足条件:size >= threshold - threshold / 4就将数组扩大为之前的两倍,同时会重新计算数组元素所处的位置并进行移动(rehash)。比如最开始数组初始大小为16,当size >= (16*2/3=10) - (10/4) = 8的时候就会扩容,将数组大小扩容至32.
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 线性探测法,寻找第一个为null的空闲位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果相同元素,替换
if (k == key) {
e.value = value;
return;
}
// 过期的脏数据,遍历的时候顺便清除脏数据,并设置新元素
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空闲位置了
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots():判断是否清理过脏数据,如果清理过脏数据,会就调用expungeStaleEntry(),留出了空位,就不需要刷新了
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
现在来看看cleanSomeSlots()
吧
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];
// 如果有脏数据就清理,并设置清理标记removed=true,在set()方法中,就不需要扩容了
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//n>>>1即n=n>>>1,查找较少的次数,比如传入n=2^7,那么查找8次,不用遍历整个tab
return removed;
}
rehash()
-
就是遍历table所有元素,如果发现了脏数据,就调用
expungeStaleEntry()
进行清除 -
再来看看size是否满足
size>=threshold*0.75
,而threshold=table.leng*2/3
,所以相当于size>=table.length*0.5
。如果满足了,就要调用resize()
进行扩容,rehash()是扩容函数resize()的唯一入口。
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
// 就是遍历table所有元素,如果发现了脏数据,就调用expungeStaleEntry()进行清除
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
来看看resize()
,简单
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 2倍扩容
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历旧table,迁移元素到新table
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// ThreadLocal已经被 回收了
if (k == null) {
// 主要是帮助GC垃圾回收器,不然这里value存在引用,后续value不用了就无法被GC回收掉,从而造成内存溢出
// 还有就是,ThreadLoca使用完了之后,一定要remove掉value
e.value = null;
} else {
int h = k.threadLocalHashCode & (newLen - 1);
// 线性探测法寻找空闲位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
删除元素
没啥说的,简单。
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) {
e.clear();
// 清理i位置的元素,并进行复位
expungeStaleEntry(i);
return;
}
}
}