简述及用法
ThreadLocal
是java中的一个线程存储类,它能够实现在指定线程内存储数据,而且在该线程中只能获取和修改该线程存储的数据,使用方式很简单:
ThreadLocal mThreadLocal =new ThreadLocal<>();
mThreadLocal.set("");
mThreadLocal.get();
接下来就去看看它的源码
ThreadLocal
先看一下ThreadLocal
的构造方法:
public ThreadLocal() {}
就是一个空方法,什么都没做
接下来通过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
,再以自身为参数获取到一个Entry
类型的对象e
,而这个e
的value
值就是我们想要获取的数据,如果e
为空就调用setInitialValue()
函数并返回其返回值;
这个ThreadLocalMap
是通过getMap
函数获取的
ThreadLocalMapgetMap(Thread t) {
return t.threadLocals;
}
可以看出这个 ThreadLocalMap
是Thread
的一个成员变量,ThreadLocalMap
的数据结构其实是一个数组,该数组的元素是Entry
类型
看一下Entry
的数据结构
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry
是一个k-v类型的数据结构,且key固定为ThreadLocal
类型,并且对该类型对象的引用为弱引用。
这时应该就能得知,调用ThreadLocal.get
函数其实就是从调用的线程中取出ThreadLocalMap
,再以ThreadLocal
为key找到ThreadLocalMap
中对应的Entry
,最终返回其value
,如果entry为空的话,会调用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;
}
protected T initialValue() {
return null;
}
这里是先通过initialValue
函数返回默认的value,再获取到线程的ThreadLocalMap
,如果不为空的话,就将默认的value存入,否则为该线程初始化ThreadLocalMap
并存入默认的value(ThreadLocal类中的initialValue返回的为null,我们可以通过继承来重写该方法设置默认的value)。
接着看一下ThreadLocal
的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);
}
可以看出set
函数和setInitialValue
函数只有value
的区别,前者使用的是传给它的参数,后者用的是默认的。
接下来要着重分析一下ThreadLocalMap
这个类
ThreadLocalMap
ThreadLocalMap
是一种哈希表,既然是哈希表,那么就一定有它的哈希算法以及处理哈希冲突的方法。
ThreadLocalMap
的元素类型为Entry
类型,它是ThreadLocalMap
的静态内部类,有key
和value
,且key
为弱引用;
ThreadLocalMap
的装载因子阈值为2/3;
ThreadLocalMap
的哈希算法为:
key.threadLocalHashCode & (len-1);
而在ThreadLoca
l中:
private final int threadLocalHashCode =nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT =0x61c88647;
可以看出,每新建一个ThreadLocal
的实例,获取它的threadLocalHashCode
时都会在nextHashCode
上累加0x61c88647(通过CAS),为什么是0x61c88647呢,官方给出的注释是说为了能让哈希码能够均匀的散列在长度为2^n的数组中,详细原理我也没怎么研究。
不过我写了一个测试Demo,从0累加16次0x61c88647,再&15
public void threadLocalHash(){
int code = 0;
for (int i = 0; i < 16; i++) {
code += 0x61c88647;
System.out.print((code & 15) + " ");
}
}
得到结果如下:
未发生一次哈希冲突,太神奇了,听说这与斐波拉契散列法黄金分割有关。
与HashMap
不同,ThreadLocalMap
是通过开放地址法来处理哈希冲突的,且是线性探测法,方法特点是:当发生哈希冲突时,顺序查看表中的下一个单元,直到找出一个空闲单元或遍历完整个表。
由于对 key
的引用为弱引用,所以表中很有可能会存在key=null
的数据,这些数据被称为脏数据(或者叫过期数据,无效数据都行,我个人喜欢这么称呼),脏数据是已经无法再使用的数据,但还占据着数组单元,会造成内存的浪费,所以ThreadLocalMap
在查询,添加,删除以及扩容的过程中都会穿插清理脏数据的过程,清理脏数据主要涉及到三个比较重要的函数:
1.replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot);
2.expungeStaleEntry(int staleSlot);
3.cleanSomeSlots(int i, int n);
下面详细讲讲这三个函数做了哪些事
replaceStaleEntry
private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {
Entry[] tab =table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i =prevIndex(staleSlot, len);
(e = tab[i]) !=null;
i =prevIndex(i, len))
if (e.get() ==null)
slotToExpunge = i;
for (int i =nextIndex(staleSlot, len);
(e = tab[i]) !=null;
i =nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k ==null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value =null;
tab[staleSlot] =new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
顾名思义,该方法的作用就是替换脏数据,tab[staleSlot]
为脏数据,需要替换成键为key
,值为value
的Entry
,就调用这个函数。
该函数主要做了两件事:替换和清除
替换规则为:
1、从staleSlot
开始往后遍历看能不能找到键为key
的数据tab[i]
直到tab[i]
为空,如果存在就将其值改为value
,并且与tab[staleSlot]
交换位置,此时tab[i]
为脏数据;
2、如果不存在键为key
的数据,就直接将tab[staleSlot]
改成新数据。
也很好理解,就是考虑到数组可能存在键为key
的数据,但发生了哈希冲突所以不在其哈希值所对应的下标位置,所以就要向后遍历,如果在遇到数据为空之前没有遍历到键为key
的数据就说明表中没有键为key
的数据,直接替换掉tab[staleSlot]
即可,否则如果遍历到tab[i]
的键为key
,就会将其值改为value
,并且与tab[staleSlot]
交换位置,此时,tab[i]
就为脏数据。
在以下三种情况下会执行清除操作:
1、首先从staleSlot
向前遍历,在遇到空数据之前如果遇到了脏数据就会进行一次清除;
2、第一种替换规则,说明一定存在脏数据;
3、替换过程中向后遍历寻找键为key
的数据时遇到了脏数据。
以上三种情况满足一种都会进行清楚操作:
cleanSomeSlots(int i, int n);
不过在replaceStaleEntry
中是这样调用的:
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
所以首先应该看一下expungeStaleEntry
方法:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab =table;
int len = tab.length;
tab[staleSlot].value =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();
if (k ==null) {
e.value =null;
tab[i] =null;
size--;
}else {
int h = k.threadLocalHashCode & (len -1);
if (h != i) {
tab[i] =null;
while (tab[h] !=null)
h =nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
同样通过函数名可以得知该函数的作用是删除脏数据,replaceStaleEntry
方法中传入的参数为最靠前的脏数据的下标,在这个方法中,首先会将tab[staleSlot]
置空,并将tab
长度减一,既然tab[staleSlot]
为空,如果后面还有数据是因为哈希冲突向后挪的,就需要补上staleSlot
的位置,于是该方法中会从staleSlot
向后遍历,在遇到空数据之前,如果遇到脏数据就要清除并将tab
的size
减一,如果不是脏数据,那么会对该数据重新做一次哈希运算,如果他所在的位置是哈希就是运算后的下标值,那么就什么都不做,否则就给他重新分配下标,并将原位置置空。
最后该方法会返回遍历到的空数据的下标,假设为i
,所以执行一次该方法可以保证数组中从staleSlot
到i
不存在脏数据
再回到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];
if (e !=null && e.get() ==null) {
n = len;
removed =true;
i = expungeStaleEntry(i);
}
}while ( (n >>>=1) !=0);
return removed;
}
它会对expungeStaleEntry
返回值i
下标后面进行扫描,扫描次数为logn(以2为底n的对数)
次,每次扫描主要做的事是:
以i
为基准值向后遍历一位,如果不为脏数据,那么i
的值就会向后推一位,再进行下一次扫描,如果为脏数据,则对这一位调用expungeStaleEntry
,并且将返回值赋给i
,再进行下一次扫描。该过程中如果清除了脏数据,则返回true
,否则返回false
。
至此为止,三个重要的函数的原理都分析完了,再带着这些去看ThreadLocalMap
的添加、删除、扩容的等方法就会容易理解很多
首先看一下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)]) {
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;
if (!cleanSomeSlots(i, sz) && sz >=threshold)
rehash();
}
很容易理解,首先计算元素哈希值,再根据对应下标进行线性探测,如果遇到了key
值相同的数据则替换value
,如果遇到null
则直接占据,如果遇到脏数据就替换,当然还有可能会进行一次脏数据清除。如果是线性探测将占据了一个空位置的情况下,则说明数组元素增加了一个,那么就会从该位置开始调用cleanSomeSlots
进行扫描清除,如果未清除脏数据且元素个数大于阈值,则会执行一次rehash
方法:
private void rehash() {
expungeStaleEntries();
if (size >=threshold -threshold /4)
resize();
}
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);
}
}
就是先清除掉整张表的脏数据,然后在判断数组中元素值是否超过了阈值的3/4,如果是则进行扩容:
private void resize() {
Entry[] oldTab =table;
int oldLen = oldTab.length;
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);
while (newTab[h] !=null)
h =nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
扩容是二倍扩容,然后再将所有元素重新散列到新表上(如果遇到脏数据就不用散列了,直接将value
置空等gc自己回收就完事了)。
get
方法:
private EntrygetEntry(ThreadLocal key) {
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);
}
private EntrygetEntryAfterMiss(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;
if (k ==null)
expungeStaleEntry(i);
else
i =nextIndex(i, len);
e = tab[i];
}
return null;
}
同样是计算key
的哈希值,再在数组中看对应下标是否是键为key
的数据,如果是就直接返回,否则线性探测,过程中如果遇到了目标元素就返回,遇到脏数据就会调用expungeStaleEntry
进行清除,遇到null
则说明数组中不存在目标数据,直接返回null
。
remove
方法:
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();
expungeStaleEntry(i);
return;
}
}
}
线性探测获取到键为key
的数据,将key
置空以后再清除。
所以说ThreadLocalMap
只要搞懂了脏数据的清除规则其他的也就很容易理解了。