ThreadLocal 源码解析与说明
这里先用图表示一下ThreadLocal在thread中的主要数据结构
Public T 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();
}
-
通过Thread.currentThread().threadLocals来获取threadLocalMap
-
当map 不为空的时候,通过map的getEntry(ThreadLocal threadLocal)方法获得Entry,然后返回entry.value所保存的泛型变量T;
-
当map为空的时候则执行setInitialValue()方法,这个方法初始化map的同时在map中创建Entry保存泛型变量T, 然后返回值。
上述过程中存在两个问题:
- threadLocalMap的getEntry方法的实现过程
- setInitialValue() 的实现过程,该过程比较简单,上述流程图可以解释
于是下面谈论threadLocalMap的getEntry方法:
private Entry getEntry(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);
}
- 通过threadLocal变量的唯一的hashcode来计算entry在map的Entry[]中的位置。
- 若数组的该位置已存在entry,并且entry所包含的key等于threadLocal,那么返回该entry。
- 否则执行getEntryAfterMiss(key, i, e)方法,在该方法内,依次遍历该位置之后的元素,key等于threadLocal则返回value,直到entry为空,返回空。
在上述的过程中,threadLocal调用的都是threadLocalMap中的方法,下面则说明map的set()方法:
set()过程:
- 根据threadLocalHashCode 计算entry的位置
- 从该位置开始向后遍历:找到第一个空位置,创建新的entry存入,如果有key等于threadLocal的entry,则替换其value,如果存在key为null,则用新的key与value覆盖之,并清除旧的数据。
- 检查entry数量是否超过阈值,超过则进行再哈希:
- 清理旧数据
- 如果此时的entry数目任然超过0.75*length, 就将数组扩大两倍,类似于hashmap的扩容方式
- 将旧entry重新哈希散列放入数组(threadHashCode & (length-1))
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
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;
//根据唯一的哈希值计算entry位置
int i = key.threadLocalHashCode & (len-1);
//在该位置及后续位置中遍历,找到一个空位,或者有相等key的entry,替换该entry的value,如果key为null,用新key、value覆盖,同时清理历史key=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;
}
}
//在找到的空位中填入新的entry
tab[i] = new Entry(key, value);
int sz = ++size;
//如果超过阀值,就需要再哈希了(再哈希中实现了扩容)
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
cleanSomeSlots(i, sz) 方法,启发式地扫描一些单元格,寻找陈旧的条目,并将这些垃圾删除掉。
下面是rehash()方法
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
// 清理一次陈旧数据
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
//清理完陈旧数据,如果>= 3/4阀值,就执行扩容,避免迟滞
if (size >= threshold - threshold / 4)
resize();
}
/**
* Double the capacity of the table.
* 把table扩容2倍,并把老数据重新哈希散列进新table
*/
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 把value也置null,有助于GC回收对象
} else {// 如果key!=null
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null) // 如果这个位置已使用
h = nextIndex(h, newLen);// 线性往后查询,直到找到一个没有使用的位置,h递增
newTab[h] = e;//在第一个空节点上塞入Entry e
count++;
}
}
}
setThreshold(newLen);// 设置新的阈值(实际set方法用了2/3的newLen作为阈值)
size = count;// 设置ThreadLocalMap的元素个数
table = newTab;// 把新table赋值给ThreadLocalMap的Entry[] table
}
/**
* Expunge all stale entries in the table.
*/
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);
}
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
//删除陈旧entry的核心方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;//删除value
tab[staleSlot] = null;//删除entry
size--;//map的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) {//key为null,执行删除操作
e.value = null;
tab[i] = null;
size--;
} else {//key不为null,重新计算下标
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {//如果不在同一个位置
tab[i] = null;//把老位置的entry置null(删除)
// 从h开始往后遍历,一直到找到空为止,插入
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
最后是threadLoalMap的remove()方法:
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//计算entry位置
int i = key.threadLocalHashCode & (len-1);
//遍历方式与set方法一样
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//clear方法很简单
e.clear();//public void clear() {this.referent = null;}
expungeStaleEntry(i);
return;
}
}
}
使用场景(有待补充)
ThreadLocal更像是一个线程级别的HashMap
在上层中(例如controller)我们把需要传送的对象(session)放在ThreadLocal中,底层(例如Dao)需要调用时候,就从ThreadLocal中拿,这样就保证了每个线程中使用到的对象都是和当前线程相关的。
- 数据库连接池中,将Connection对象方法Threadlocal中,保证线程安全与事务;
- 同理,在实现redis事务中,也可以用Threadlocal保存Multi对象;