概述
ThreadLocal 译为线程本地变量,适用场景有很多总结起来就是一个:只希望在本线程使用的变量。
用户从浏览器发起请求,服务器有相应的线程处理请求,但是http无状态,session 可以用来保存一些状态只与用户此次会话相关,此时我们就可以使用线程本地变量来保存session。注意一个线程不是为一个用户服务的,当使用完成之后进行remove 本地变量,防止内存泄漏。
同一个线程处理非常复杂业务的时候,可以将显式传递的参数当作线程本地变量,从而可以在多个方法中使用。
在数据库连接的时候可以使用ThreadLocal
Entry 设计为继承WeakReference ,其Key 则变为了弱引用,这样当线程执行完任务退出时,相应的栈帧退出后,threadlocal无强引用,此时的弱引用只存在于堆中的Map 的key,正因为key是弱引用之后在gc时会被回收。我们常说的内存泄漏其实是关于value 的使用,value 指向的对象可为强引用,如果不去保证释放,则会造成map中存在key为null,value 不为null 的entry,造成内存泄漏。
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();
//如果从当前i 向后遍历table,找到entry 的k 正好为 需要set 进去的key,此时只需修改value 就好
if (k == key) {
e.value = value;
return;
}
//如果k == null 说明此entry 为stale ,此时我们应该明白不该直接将key value设置到该位置上,因为在该位置到后续entry为null 位置的前一个位置上可能有entry的key为我们设置的key,如果是这种情况我们应该找到该
//entry 并设置value,同时把该entry 设置到stalelot 上。如果未找到,那么当然就在该位置上直接创建。实际情况伴随着stale entry 的清除,具体逻辑见下面replaceStaleEntry。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//替换过期entry 的方法
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
//从后向前找过期的entry的索引,直到遇到一个null entry 停止,此时记录的slotToExpunge即为staleslot 之前的最早的过期entry,之后我们清理的时候可以从该entry 开始。
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
//从staleslot 开始,从当前向后记录过期entry ,如果遍历的过程中遇到和当前要set 的key相等的k,说明我们可以将新entry 设置到staleslot 上,然后把staleslot 上的entry 拿到该索引位置上。
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
//如果staleslot 之前没有过期entry,此时更新slotToExpunge 为当前索引
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//执行到此,说明staleslot 之后没有与其哈希冲突的entry,所以应将staleslot 位置设置为我们应该插入的位置,同时进行清除过期entry。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
//如果slotToExpunge == staleslot 说明staleslot 之前 之后 都没有过期slot 自然不需要清理否则都需要清理。清除逻辑见expungeStaleEntry和cleanSomeSlots
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
//清除过期entry 返回值为null entry 的索引。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果为null 代表找到过期entry
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;
}
//采用启发算法 删除过期entry 做到logn的时间复杂度 达到 和O(n)差不多的效果
// 返回值表示是否删除成功。
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;
}
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
//当索引位置上k 与 key不相同时线性探测
return getEntryAfterMiss(key, i, e);
}
//需要注意的是该方法在发现过期entry 时会及时进行清除。
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)
//清除之后后面的hash冲突结点会移到正确的位置,所以,此时不会进行i = nextIndex(i, len);
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return 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) {
//主动将key对应的引用队列出队列,即删除。
e.clear();
//删除对应的entry
expungeStaleEntry(i);
return;
}
}
}