簡介
java.lang.ThreadLocal類,自JDK1.2版本就加入了Java,ThreadLocal類可以給每個線程維護一個獨立的變量副本,使多線程的場景使用共有的ThreadLocal變量,同時每個線程在ThreadLocal對象中儲存的變量副本是互相隔離的。
調用ThreadLocal的
public void set(T value)
方法,就可以為目前線程設定一個線程專有的變量。
調用ThreadLocal的
public T get()
方法,可以獲得目前線程專有的變量。
下面是一個簡單的使用ThreadLocal的例子:
static class MyThread extends Thread {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
@Override
public void run() {
super.run();
for (int i = 0; i < 3; i++) {
threadLocal.set(i);
System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
}
}
}
public static void main(String[] args) {
MyThread myThreadA = new MyThread();
myThreadA.setName("ThreadA");
MyThread myThreadB = new MyThread();
myThreadB.setName("ThreadB");
myThreadA.start();
myThreadB.start();
}
輸出的結果是:
ThreadA threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2
可以看到,雖然兩個線程使用了一個ThreadLocal的靜态變量,但是get()方法得到的值都是0,1,2三個值,說明兩個線程在set值的時候值是隔離的。
通過get()和set()方法的源碼,我們可以知道ThreadLocal維護了怎樣的資料結構,從了解ThreadLocal如何使線程間的變量互相隔離。
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();
}
首先拿到目前線程,然後把目前線程當參數傳給了getMap()方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
傳回的是目前線程的threadLocals屬性。下面就是ThreadLocal資料結構中最繞的一部分, Thread和ThreadLocal兩個類共同搭建了一套線程隔離系統。
Thread類中threadLocals屬性的定義是這樣的:
ThreadLocal.ThreadLocalMap threadLocals = null;
這是一個在ThreadLocalMap中定義的靜态内部類,是個Map,裡面維護了一個Entry類的數組。這個Entry是在ThreadLocalMap類中定義的内部類:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到這個Entry繼承WeakReference類,對key使用的是弱引用。
回到之前的get()代碼,獲得Thread中的ThreadLocalMap後,判斷是否存在。
如果ThreadLocalMap不存在則調用setInitialValue()方法初始化。
如果ThreadLocalMap存在,則從map中獲得Entry,用的key就是目前ThreadLocal對象,也就是這一行:
ThreadLocalMap.Entry e = map.getEntry(this);
然後從Entry中拿到value,就是get()方法要的結果。
ThreadLocal的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);
}
邏輯也很簡單,也是先獲得目前線程,然後獲得目前線程的ThreadLocalMap,然後set進去。如果ThreadLocalMap還沒建立,則調用createMap()方法建立。
get()方法中的setInitialValue()方法和set()方法中的createMap()方法都是建立ThreadLocalMap,而且建立完了都往裡面寫了一個Entry對象,key都是ThreadLocal對象,差別在于setInitialValue()方法中設定的value是ThreadLocal配置的預設值,createMap()方法中設定的value就是我們在set()方法中傳的值。
綜上可知,每個線程都維護了一個ThreadLocalMap,ThreadLocalMap中 有Entry數組,Entry的key是ThreadLocal對象,Entry的value就是線程隔離的值。
ThreadLocal的資料結構不太好了解,主要是因為這個類裡set()的值實際上是二維的,就像二維數組的值一樣,下面的圖有助于了解ThreadLocal維護了一個什麼資料結構:
是以,這個資料結構反過來就好了解一些,實際上是每個線程維護了一個map,而每個ThreadLocal對象就是map的key。
關于記憶體洩露:
上圖的虛線表示弱引用。
以下這些是強引用:
1,ThreadLocalMap對Entry的引用。
2,Entry對value的引用。
注意到set()方法中有這麼一行:
map.set(this, value);
ThreadLocalMap的set()方法是這樣的:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because i
// least as common to use set() to create new entri
// it is to replace existing ones, in which case, a
// 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();
}
可以看到,當ThreadLocalMap打算往Entry裡set一個值,循環數組的時候順便還判斷了一下每個Entry的key值,如果是key是null的話就調用replaceStaleEntry()方法:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
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.
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.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
這個方法可以把key是null的Entry的value設為null,進而釋放對value對象的引用。
在ThreadLocalMap的set()方法的最後,調用了rehash()方法:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
方法本身是用來擴容的,但是方法開始的時候調用的expungeStaleEntries()方法會把key是null的Entry設為null,釋放對Entry的引用:
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);
}
}
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;
}
另外,在ThreadLocal的get()方法中也調用了這個expungeStaleEntries()方法。說明get()/set()方法都能幫助緩解記憶體洩露,ThreadLocal對象被回收後,各線程中的Entry,Entry中的value,都可以被回收,防止記憶體洩露。
如果一直不調用get()和set()方法那上面的套路就無效了,是以,手動調用ThreadLocal的remove()方法,是比較靠譜的做法。
完