解决了线程上下文中的变量传递问题。
先看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);
}
|
先获取本线程 t,然后getMap获取一个ThreadLocalMap ,如果ThreadLocalMap存在就把把当前threadLocal作为key,值value作为value,放到
此ThreadLocalMap中,如果不存在此map就创建
那么ThreadLocalMap到底是什么东东呢
static
class
ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static
class
Entry
extends
WeakReference
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super
(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private
static
final
int
INITIAL_CAPACITY =
16
;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private
Entry[] table;
/**
* The number of entries in the table.
*/
private
int
size =
0
;
/**
* The next size value at which to resize.
*/
private
int
threshold;
// Default to 0
}
|
初看跟hashmap的结构差不多
先放张图,理解下
java.lang.Thread 内有个 ThreadLocalMap :
1
|
ThreadLocal.ThreadLocalMap threadLocals =
null;
|
ThreadLocalMap 存储了所有跟该Thread绑定的threadLocal对象到该ThreadLocal对象对应value的映射。每次在读取一个ThreadLocal对象的时候
就是通过ThreadLocalMap来查看ThreadLocal对应对象对应的值是什么
因为每个Thread都有自己的ThreadLocalMap,所以统一ThreadLocal对象在不同的Thread里能对应不同的value
核心讲下ThreadLocalMap
从上文的讨论中,我们发现,不管是ThreadLocal的底层实现结构,还是ThreadLocal的API中,最核心的就是这个ThreadLocalMap
。
下面我们就来好好分析下这个ThreadLocalMap。
在详细讲解ThreadLocalMap之前,我们先要了解ThreadLocalMap的Hash冲突处理,因为这是整个ThreadLocalMap最核心的地方,理解了这个
ThreadLocalMap其他的内容也就比较好理解了。
首先我们回顾下Java中的HashMap,我们知道HashMap的实现方式是 数组 + 链表,其中数组用于Hash桶定位,链表用于解决Hash冲突。
ThreadLocalMap,本质上来讲,也是一个Map,也用到了Hash算法,那么它在实现上与HashMap有什么区别呢?这里先把结论给出来:
Hash冲突的处理方式不一样,HashMap使用 链地址法 来解决Hash冲突,而ThreadLocalMap使用 开放地址法 来解决Hash冲突。
每一个 ThreadLocal 对象都有一个 threadLocalHashCode,在将 ThreadLocal 对象及其对应 Value 放入 ThreadLocalMap 时,先根据
threadLocalHashCode 和 ThreadLocalMap 内数组大小用类似于 threadLocalHashCode % ThreadLocalMap.length() 的方法计算出来
该 threadLocalHashCode 对应的哪一个 Slot 的 index,再构造 Entry 放入该 index 指向的 ThreadLocalMap 的 Slot 中。
因为 ThreadLocalMap 内数组大小有限,类似于 threadLocalHashCode % ThreadLocalMap.length()
计算 index 的方法可能
出现两个不同的 ThreadLocal 对象带着两个不同的 threadLocalHashCode 但被 Hash 到同一个 Slot 的情况(即发生Hash冲突)
如下图 ThreadLocalA ThreadLocalB ThreadLocalC 都具有相同的 threadLocalHashCode,在插入 ThreadLocalC 时,根据其
threadLocalHashCode 先被 Hash 到 ThreadLocalA 的 Slot,发现 Slot 不为空,于是 index + 1
再判断临近的已经存了 ThreadLocalB
的 Slot 是否为空,不为空则继续 index + 1
直到找到一个空的 Slot 将 ThreadLocalC 存入。
也就是说,ThreadLocalMap发生Hash冲突时,会顺着当前Hash桶往下找(index + 1),直到找到一个为空的桶,然后将Entry放入。
这种碰撞处理方式也就导致:
每个 ThreadLocal 对象的 threadLocalHashCode 不能挨得太近,不然冲突会很多。
其计算方法如下:
1
2
|
private
static AtomicInteger nextHashCode =
new AtomicInteger();
private
final
int threadLocalHashCode = nextHashCode.getAndAdd(
0x61c88647)
|
0x61c88647 是个很神奇的数字,据说是来自斐波那契散列,用这个数字,产生Hash冲突的概率较低。
ThreadLocalMap 一定不能完全装满,内置的数组一定要比实际存入的 ThreadLocal 对象至少大 1。事实上 ThreadLocalMap 的 Load Factor
是 2/3,超过之后会 rehash 并扩容。不能完全装满的原因是比如还是上面那张图,要获取一个 ThreadLocal 对象对应的 Value,并且这个目标
ThreadLocal 没有存入该 ThreadLocalMap,它的 threadLocalHashCode 刚好也 Hash 到了 ThreadLocalA 对象所在的 Slot,获取的时候先判断
目标 ThreadLocal 是不是等于 ThreadLocalA,不是的话再判断是不是等于 ThreadLocalB,依次类推直到获取到一个空 Slot 从而才能知道该
ThreadLocal 没有存储在当前 ThreadLocalMap 中。如果 ThreadLocalMap 完全装满,就不能依赖这个 Slot 是否为空的判断了;
讲完了ThreadLocalMap最核心的Hash冲突处理后,我们来看看相关的API。
从上文分析ThreadLocal的API过程中,我们发现,与ThreadLocal相关的的API有:
下面将详细介绍这些API以及相关的源码。
在前面分析ThreadLocal的set方法中,我们知道,如果当前Thread对应的ThreadLocalMap为null,
则会调用createMap方法创建ThreadLocalMap:
void
createMap(Thread t, T firstValue) {
t.threadLocals =
new
ThreadLocalMap(
this
, firstValue);
}
即调用了ThreadLocalMap的构造函数,我们来看看构造函数源码:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}
|
同样,注释里面说的很明白:创建一个新的map,并将firstKey、firstValue存入map。
我们看这里的代码,是不是跟HashMap很类似?包括Entry数组table、Hash桶定位过程中的按位与。
这里我们需要特别的关注下Entry对象。
static
class
Entry
extends
WeakReference
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super
(k);
value = v;
}
}
|
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
);
//定位hash桶的位置
for
(Entry e = tab[i];
e !=
null
;
e = tab[i = nextIndex(i, len)]) {
//从当前桶向下遍历
ThreadLocal> k = e.get();
if
(k == key) {
//如果key存在就替换掉原来的value
e.value = value;
return
;
}
if
(k ==
null
) {
//如果当前桶位置key为null,特殊处理,替换并清除过期的Entry
replaceStaleEntry(key, value, i);
return
;
}
}
tab[i] =
new
Entry(key, value);
//找到一个为null的桶,将传入的Entry放入当前桶
int
sz = ++size;
if
(!cleanSomeSlots(i, sz) && sz >= threshold)
// 6. 没有清理出可用的桶而且容量超过阈值,重新Hash
rehash();
}
|
set方法,作用很明显,跟HashMap的put方法一样,就是将键值对放入map。当然,考虑到会有Hash冲突,所以需要特殊处理(处理方式前文已经介绍)。
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private
Entry getEntry(ThreadLocal> key) {
// 1. 定位Hash桶位置
int
i = key.threadLocalHashCode & (table.length -
1
);
// 2. 获取当前桶位置的Entry
Entry e = table[i];
// 3. 如果Entry不为null且可以相同,说明Hash命中,返回对应的值即可
if
(e !=
null
&& e.get() == key)
return
e;
// 4. 未命中,特殊处理
else
return
getEntryAfterMiss(key, i, e);
}
|
get方法的作用,也无需多说,跟HashMap的get方法一样,根据key去找value。同样,考虑到Hash冲突,会有未命中的情况,需要做特殊处理,即调用getEntryAfterMiss方法:
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private
Entry getEntryAfterMiss(ThreadLocal> key,
int
i, Entry e) {
Entry[] tab = table;
int
len = tab.length;
// 1. 从当前位置往下找,这个原因在Hash冲突处理章节已经介绍过:
// 插入时,如果当前位置已经有元素,则往下找一个位置,看是否为null,
// 如此反复,直到找到一个为null的位置,把Entry放入该位置
// 所以,查找的时候,也是一样的逻辑,如果根据key算出来的Hash值位置上,不是当前Entry,
// 那么就顺着数组往下找
while
(e !=
null
) {
ThreadLocal> k = e.get();
// 2. 如果key相同,说明找到,直接返回
if
(k == key)
return
e;
// 3. 如果当前位置key为null,特殊处理:清除过期的Entry
if
(k ==
null
)
expungeStaleEntry(i);
// 4. 继续找数组的下一个位置
else
i = nextIndex(i, len);
e = tab[i];
}
// 最后还是没有找到,返回null
return
null
;
}
|
/**
* Remove the entry for key.
*/
private
void
remove(ThreadLocal> key) {
Entry[] tab = table;
int
len = tab.length;
// 1. 定位Hash桶位置
int
i = key.threadLocalHashCode & (len-
1
);
// 2. 遍历找出key对应的Entry,
for
(Entry e = tab[i]; e !=
null
; e = tab[i = nextIndex(i, len)]) {
if
(e.get() == key) {
// 3. 找到对应的Entry后,清理该Entry的弱引用
e.clear();
// 4. 特殊处理:清除过期的Entry
expungeStaleEntry(i);
return
;
}
}
}
|