背景(注释):
ThreadLocal提供了线程私有变量。每一个变量对于每个线程来说都会有唯一的一个副本,通过get方法得到或者set方法设置,独立于其他线程的版本。ThreadLocal变量是典型的字段,只用于当前的一个线程(比如userID或者TransactionID)。
一下的这个用例给了每个线程一个ID:
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
每个线程都隐含有一个变量,当线程存活着并且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,即threadLocals。
- 假如不为空则,从中根据当前ThreadLocal作为key来获取Entry对象,不为空则返回value。
- 假如当前map为空,则调用setInitialValue来初始化。
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);
}
注意:是从Thread对象中得到ThreadLocalMap对象,事实上每个Thread都有个Map。
- 如果map还未初始化,则先初始化(创建16个Entry的table),然后放入第一个Entry(key, value),这里的Entry继承WeakReference<ThreadLocal<?>>
- 假如map已经存在,则调用set方法放入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();
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();
}
使用hashCode定位下标,假如发现存在“脏”(因为Entry继承WeakReference,当ThreadLocal即我们的key被垃圾回收后,会是null)的Entry则取代,否则将会创建一个新的Entry。值得注意的是,不管那种情况都会尝试删除已经“脏”的数据,增加的时候还会看是否需要扩展底层Entry组。
我们接着来看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);
}
详情如下:
- 首先取得staleSlot前面的不为null的Entry中最前面的staleEntry(slotToExpunge,默认为staleSlot)。
- 然后从staleSlot后面开始查询,假如发现“脏”数据,则在slotToExpunge==staleSLot的情况下更新slotToExpunge。假如找到同样的k,则更新Entry,并且置换staleSlot和当前的Entry,然后试着清理当前的脏数据。
- 假如最后探测到null的Entry,则清理如果需要的话清理slotToExpunge上的脏数据。
以上的规则是slotToExpunge保证是连续不为null的Entry中最前面的“脏”数据,当slotToExpunge不为staleSlot时需要清理。
expungeStaleEntry:
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();
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;
}
尝试在下标staleSlot上清理脏数据,并且连续清理直到遇见null,过程中需要时会做rehash()。
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;
}
cleanSomeSlots通过长度n以2为底数得到探测的次数。
重点是,以上代码中出现的e.get()==null是极为关键的,由于e是扩展WeakReference,所以当我们的key不存在引用时,只存在若引用的ThreadLocal很可能会被入队(引用队列),所以通过Entry得到的ThreadLocal可能为空。
这一套机制这里适合用于做Entry数组上的清理操作。