Java 中的 ThreadLocal是線程內的局部變量, 它為每個線程保存變量的一個副本。ThreadLocal 對象可以在多個線程中共享, 但每個線程只能讀寫其中自己的副本。
目錄:
代碼示例
我們編寫一個簡單的示例:
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author finley
*/
public class MyThread extends Thread {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static final Random random = new Random();
@Override
public void run() {
int localValue = random.nextInt();
threadLocal.set(localValue);
System.out.println("Thread: " + Thread.currentThread().getName() + " set thread local: " + localValue);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread: " + Thread.currentThread().getName() + " threadLocal: " + threadLocal.get() + ", local: " + localValue);
}
public static void main(String[] args) {
int concurrent = 10;
ExecutorService service = Executors.newFixedThreadPool(concurrent);
for (int i = 0; i < concurrent; i++) {
service.execute(new MyThread());
}
service.shutdown();
}
}
運行結果:
Thread: pool-1-thread-1 set thread local: -1262320606
Thread: pool-1-thread-2 set thread local: 1222545653
Thread: pool-1-thread-3 set thread local: 2067394038
Thread: pool-1-thread-4 set thread local: 920362206
Thread: pool-1-thread-5 set thread local: -1977931750
Thread: pool-1-thread-6 set thread local: 985735150
Thread: pool-1-thread-7 set thread local: -602752866
Thread: pool-1-thread-8 set thread local: 672437027
Thread: pool-1-thread-9 set thread local: 1063652674
Thread: pool-1-thread-10 set thread local: 1790288576
Thread: pool-1-thread-1 threadLocal: -1262320606, local: -1262320606
Thread: pool-1-thread-3 threadLocal: 2067394038, local: 2067394038
Thread: pool-1-thread-4 threadLocal: 920362206, local: 920362206
Thread: pool-1-thread-6 threadLocal: 985735150, local: 985735150
Thread: pool-1-thread-7 threadLocal: -602752866, local: -602752866
Thread: pool-1-thread-2 threadLocal: 1222545653, local: 1222545653
Thread: pool-1-thread-5 threadLocal: -1977931750, local: -1977931750
Thread: pool-1-thread-8 threadLocal: 672437027, local: 672437027
Thread: pool-1-thread-10 threadLocal: 1790288576, local: 1790288576
Thread: pool-1-thread-9 threadLocal: 1063652674, local: 1063652674
可以看到10個線程調用同一個ThreadLocal對象的set方法寫入隨機值, 然后讀取到自己寫入的副本。
源碼解析
我們從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);
}
set方法將當前線程的副本寫入了一個ThreadLocalMap, map的key是當前的ThreadLocal對象。
接下來通過getMap方法分析這個ThreadLocalMap是如何維護的:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
每個 Thread 對象維護了一個 ThreadLocalMap 類型的 threadLocals 字段。
ThreadLocalMap 的 key 是 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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
首先獲得當前線程的ThreadLocalMap, 然后從 ThreadLocalMap 嘗試獲得當前 ThreadLocal 對象對應的副本。
若獲取失敗,則寫入並返回initialValue
方法定義的默認值。
Thread.threadLocals 字段是惰性初始化的。 ThreadLocal.set() 方法發現 threadLocals 為空時會調用 createMap 方法進行初始化, ThreadLocal.get()方法同樣會在setInitialValue() 中調用 createMap 方法初始化 Thread.threadLocals 字段。
為了不影響讀者整體了解ThreadLocal, ThreadLocalMap 的實現原理在最后一節ThreadLocalMap
InheritableThreadLocal
InheritableThreadLocal 在子線程創建時將父線程的變量副本傳遞給子線程。
InheritableThreadLocal 繼承了 ThreadLocal 並重寫了3個方法, 它使用 Thread.inheritableThreadLocals 代替了 Thread.threadLocals 字段。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
ThreadLocalMap 的構造器中實現了向子線程傳遞的邏輯:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
Thread.init
方法調用此構造器傳遞 InheritableThreadLocal:
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
ThreadLocalMap
值得一提的是, ThreadLocalMap 中使用的是 WeakReference, 當 ThreadLocal 對象不再被外部引用時, 弱引用不會阻止GC因此避免了內存泄露。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 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
}
Entry 的 key 始終是 ThreadLocal 對象, 值則是 ThreadLocal 對象綁定的變量副本。
Get 流程
首先來看 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);
}
利用 table 大小始終為2的整數冪的特點使用位運算找到哈希槽。
若哈希槽中為空或 key 不是當前 ThreadLocal 對象則會調用getEntryAfterMiss
方法:
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)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
ThreadLocalMap 使用開放定址法處理哈希沖突, nextIndex 方法會提供哈希沖突時下一個哈希槽的位置。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
nextIndex 返回下一個位置, 到達末尾后返回第一個位置0.
getEntryAfterMiss 方法會循環查找直到找到或遍歷所有可能的哈希槽, 在循環過程中可能遇到4種情況:
- 哈希槽中是當前ThreadLocal, 說明找到了目標
- 哈希槽中為其它ThreadLocal, 需要繼續查找
- 哈希槽中為null, 說明搜索結束未找到目標
- 哈希槽中存在Entry, 但是 Entry 中沒有 ThreadLocal 對象。因為 Entry 使用弱引用, 這種情況說明 ThreadLocal 被GC回收。
為了處理GC造成的空洞(stale entry), 需要調用expungeStaleEntry
方法進行清理。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理當前的空洞
tab[staleSlot].value = null;
tab[staleSlot] = null;
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) {
// 若下一個位置還是空洞將其一並清除
e.value = null;
tab[i] = null;
size--;
} else {
// 發現不是空洞的 Entry 將其放入最靠前的哈希槽中
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null) // 處理移動過程中的哈希沖突
h = nextIndex(h, len);
tab[h] = e;
}
}
// 循環執行直到遇到空的哈希槽, 表明從 staleSlot 開始的查找序列中間不會存在空哈希槽或空Entry
}
return i;
}
清理分為兩個部分:
- 首先清理掉空的Entry
- Entry被清理后可能會使 getEntryAfterMiss 方法誤以為搜索已經結束,因此需要將后面的 Entry 進行 rehash 填補空洞
在執行清理時, 可能因為GC造成多個空洞因此需要循環清理。
Set 流程
首先來看 ThreadLocalMap.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;
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();
}
首先計算哈希槽的位置, 此時可能有3種情況:
- 哈希槽為空, 直接將新 Entry 填入槽中; 此外調用 cleanSomeSlots 搜索並清理 GC 造成的空洞; 此外檢查 Entry 數量是否到達閾值, 必要時調用 rehash 方法進行擴容。
- 哈希槽中為當前 ThreadLocal, 直接進行替換
- 哈希槽中為空 Entry, 說明原有ThreadLocal 被 GC 回收, 調用 replaceStaleEntry 將其替換。
接下來重點分析 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);
}
replaceStaleEntry 方法看上去非常復雜, 簡單的說分為三部分:
- 向前尋找空的 Entry 將其位置寫入 slotToExpunge, 這是為了清理不必繼續關注
- 向后進行尋找若是找到與傳入的 key 相同 Entry 則更新 Entry 的內容並將其移動到 staleSlot, 然后調用 cleanSomeSlots 進行清理
- 若最終沒有找到 key 相同的Entry, 則在 staleSlot 處寫入一個新的 Entry, 調用 cleanSomeSlots 進行清理
cleanSomeSlots 調用 expungeStaleEntry 從位置 i 開始向后清理。
執行log2(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;
}
簡單看一下 rehash 的過程:
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
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
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
首先進行清理,若清理后sz > thresholde * 0.75
將哈希表的的大小翻倍。
Remove
remove 方法和 get 方法比較類似:
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) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}