ThreadLocal在工作中经常使用,尤其在Web的请求周期中,常见的使用场景如在SpringMvc中,从登录拦截器中preHandler中set一个全局的变量,在postHanlder时remove,这里很多使用容易忽略remove,因为大多数Web服务器是使用线程池工作的,如果在一个request中不remove操作,而在第二次请求中又因为某些业务没有在拦截器中做set操作,则而第二次从ThreadLocal中获取的值变是一个失效值,这点是比较容易忽视的一个问题。
另外如果是在一个request中新建一个新线程,在子线程中同样是拿不到这个ThreadLocal的,这时使用InheritableThreadLocal便可以解决这个。
本文不对ThreadLocal的使用场景做过多介绍,下面只对源码进行一个分析,如果有不对之处还望指正。
1、get方法没什么多介绍的,从当前线程的map中获取Entry,如果存在便返回,否则设置初始值。setInitialValue默认是null,并在其中做初始化map的工作。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
2、set方法与remove方法也很简单,这里也不做过多介绍,接下来重点需要解释的是ThreadLocalMap以及Entry这两个内部类,因为ThreadLocal的get和set的功能都是基于这两个类完成的,先看ThreadLocalMap。
ThreadLocal内部初始化了16个Entry数组的table,并设置阈值达到在2/3的时候做自动扩容,而table的长度必须为2的幂,这些在构造函数里面,其中Entry的位置是通过threadLocalHashCode与15做与操作得到的,这里的threadLocalHashCode是ThreadLocal的一个从0开始自增的值。
threadLocalHashCode的值是从0开始每次自增1100001110010001000011001000111,举个例子,当初始长度为16时,threadLocalHashCode的值会是
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 10 1 8的循环。
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);
}
第二个构造方法的作用是复制一个ThreadLocalMap,这个方法在Thread类里面调用,当new一个Thread的时候,会将父线程的inheritableThreadLocals这个ThreadLocalMap复制到当前线程的inheritableThreadLocals中,这样形成一个链表,即使这个子线程再new一个线程,子子线程同样可以获取到所以父线程的inheritableThreadLocals,传递性得以保持。
if (parent.inheritableThreadLocals != null)//Thread初始化时的操作
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
ThreadLocal key = e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);//注意着,因为复制时需要一次新的寻址,并不是单纯的按位复制。
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
获取Entry的方法同样是先用hash获取位置,如果发现当前Entry为空或者发现Entry中的key与当前的ThreadLocal不是同一个,则继续寻找,在getEntryAfterMiss中继续寻找,如果找了一圈仍然没有找到,则返回null,这里需要注意,在一个固定长度的table中,例如16,table的顺序永远是按照7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0这样的顺序set进去的。在查询过程中,如果某一个entry的key为null,则说明这个key已经被回收了,则需要对该Entry进行回收清理,否则该value得不到清理就会有内存泄漏。
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;
if (k == null)
expungeStaleEntry(i);//清理当前Entry
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;//显示置为null,以便gc可以回收
tab[staleSlot] = null;//
size--;
// Rehash until we encounter null清理完了之后再继续循环一遍,并重新将entry hash一遍。
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;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
ThreadLocal.set的时候调用的是也是ThreadLocalMap的set方法,这个方法先找到位置i,再发table数组中找当前的Entry,如果Entry的key与当前ThreadLocal一致,那是最好了,直接将Entry的value替换,如果为(k=null),则说明这个key被回收了,则重置便好了,如果最后循环一遍直到Entry为null,说明这是一个新的ThreadLocal,则new一个新的Entry,并对table重新清理。
private void set(ThreadLocal key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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);//这里是因为key为null,但是当前entry没有清理,便可以复用。
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)//如果是新增了entry,需要重新做一道清理,如果大于阈值还需要重新分配。
rehash();
}
其实ThreadLocal类代码不多,核心思想我认为有以下三点:
1、理解Entry是一个WeakReference,否则的话在线程池场景中,一直复用线程,即使ThreadLocal的作用域已经不再,但是因为在ThreadLocalMap中被引用,这样就会造成内存泄漏。
2、另外一个就是起清理弱引用的value的expungStaleEntry方法,这个方法在WeakHashMap中也有,思路类似,可以做个对比区分。
3、threadLocalHashCode的使用很巧,在多线程场景中是解决hash冲突的另外一种可以借鉴的方式,将多个ThreadLocal分散到不同的位置,在hash冲突时可减少重新寻找的次数。