1. 概念
ThreadLocal 用于提供線程局部變量,在多線程環境可以保證各個線程里的變量獨立于其它線程里的變量。也就是說 ThreadLocal 可以為每個線程創建一個【單獨的變量副本】,相當于線程的 private static 類型變量。
ThreadLocal 的作用和同步機制有些相反:同步機制是為了保證多線程環境下數據的一致性;而 ThreadLocal 是保證了多線程環境下數據的獨立性。
2. 使用示例
public class ThreadLocalTest {
private static String strLabel;
private static ThreadLocal<String> threadLabel = new ThreadLocal<>();
public static void main(String... args) {
strLabel = "main";
threadLabel.set("main");
Thread thread = new Thread() {
@Override
public void run() {
super.run();
strLabel = "child";
threadLabel.set("child");
}
};
thread.start();
try {
// 保證線程執行完畢
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("strLabel = " + strLabel);
System.out.println("threadLabel = " + threadLabel.get());
}
}
運行結果:
strLabel = child
threadLabel = main
從運行結果可以看出,對于 ThreadLocal 類型的變量,在一個線程中設置值,不影響其在其它線程中的值。也就是說 ThreadLocal 類型的變量的值在每個線程中是獨立的。
3. ThreadLocal 實現
ThreadLocal 是怎樣保證其值在各個線程中是獨立的呢?下面分析下 ThreadLocal 的實現。
ThreadLocal 是構造函數只是一個簡單的無參構造函數,并且沒有任何實現。
3.1 set(T value) 方法
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(T value) 方法中,首先獲取當前線程,然后在獲取到當前線程的 ThreadLocalMap,如果 ThreadLocalMap 不為 null,則將 value 保存到 ThreadLocalMap 中,并用當前 ThreadLocal 作為 key;否則創建一個 ThreadLocalMap 并給到當前線程,然后保存 value。
ThreadLocalMap 相當于一個 HashMap,是真正保存值的地方。
3.2 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();
}
同樣的,在 get() 方法中也會獲取到當前線程的 ThreadLocalMap,如果 ThreadLocalMap 不為 null,則把獲取 key 為當前 ThreadLocal 的值;否則調用 setInitialValue() 方法返回初始值,并保存到新創建的 ThreadLocalMap 中。
3.3 initialValue() 方法:
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;
}
...
initialValue() 是 ThreadLocal 的初始值,默認返回 null,子類可以重寫改方法,用于設置 ThreadLocal 的初始值。
3.4 remove() 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal 還有一個 remove() 方法,用來移除當前 ThreadLocal 對應的值。同樣也是同過當前線程的 ThreadLocalMap 來移除相應的值。
3.5 當前線程的 ThreadLocalMap
在 set,get,initialValue 和 remove 方法中都會獲取到當前線程,然后通過當前線程獲取到 ThreadLocalMap,如果 ThreadLocalMap 為 null,則會創建一個 ThreadLocalMap,并給到當前線程。
...
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
可以看到,每一個線程都會持有有一個 ThreadLocalMap,用來維護線程本地的值:
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
在使用 ThreadLocal 類型變量進行相關操作時,都會通過當前線程獲取到 ThreadLocalMap 來完成操作。每個線程的 ThreadLocalMap 是屬于線程自己的,ThreadLocalMap 中維護的值也是屬于線程自己的。這就保證了 ThreadLocal 類型的變量在每個線程中是獨立的,在多線程環境下不會相互影響。
4. ThreadLocalMap
4.1 構造方法
ThreadLocal 中當前線程的 ThreadLocalMap 為 null 時會使用 ThreadLocalMap 的構造方法新建一個 ThreadLocalMap:
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);
}
構造方法中會新建一個數組,并將將第一次需要保存的鍵值存儲到一個數組中,完成一些初始化工作。
4.2 存儲結構
ThreadLocalMap 內部維護了一個哈希表(數組)來存儲數據,并且定義了加載因子:
// 初始容量,必須是 2 的冪
private static final int INITIAL_CAPACITY = 16;
// 存儲數據的哈希表
private Entry[] table;
// table 中已存儲的條目數
private int size = 0;
// 表示一個閾值,當 table 中存儲的對象達到該值時就會擴容
private int threshold;
// 設置 threshold 的值
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
table 是一個 Entry 類型的數組,Entry 是 ThreadLocalMap 的一個內部類。
4.3 存儲對象 Entry
Entry 用于保存一個鍵值對,其中 key 以弱引用的方式保存:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4.4 保存鍵值對
調用 set(ThreadLocal key, Object value) 方法將數據保存到哈希表中:
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計算要存儲的索引位置
int i = key.threadLocalHashCode & (len-1);
// 循環判斷要存放的索引位置是否已經存在 Entry,若存在,進入循環體
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
// 若索引位置的 Entry 的 key 和要保存的 key 相等,則更新該 Entry 的值
if (k == key) {
e.value = value;
return;
}
// 若索引位置的 Entry 的 key 為 null(key 已經被回收了),表示該位置的 Entry 已經無效,用要保存的鍵值替換該位置上的 Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 要存放的索引位置沒有 Entry,將當前鍵值作為一個 Entry 保存在該位置
tab[i] = new Entry(key, value);
// 增加 table 存儲的條目數
int sz = ++size;
// 清除一些無效的條目并判斷 table 中的條目數是否已經超出閾值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 調整 table 的容量,并重新擺放 table 中的 Entry
}
首先使用 key(當前 ThreadLocal)的 threadLocalHashCode 來計算要存儲的索引位置 i。threadLocalHashCode 的值由 ThreadLocal 類管理,每創建一個 ThreadLocal 對象都會自動生成一個相應的 threadLocalHashCode 值,其實現如下:
// ThreadLocal 對象的 HashCode
private final int threadLocalHashCode = nextHashCode();
// 使用 AtomicInteger 保證多線程環境下的同步
private static AtomicInteger nextHashCode =
new AtomicInteger();
// 每次創建 ThreadLocal 對象是 HashCode 的增量
private static final int HASH_INCREMENT = 0x61c88647;
// 計算 ThreadLocal 對象的 HashCode
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
在保存數據時,如果索引位置有 Entry,且該 Entry 的 key 為 null,那么就會執行清除無效 Entry 的操作,因為 Entry 的 key 使用的是弱引用的方式,key 如果被回收(即 key 為 null),這時就無法再訪問到 key 對應的 value,需要把這樣的無效 Entry 清除掉來騰出空間。
在調整 table 容量時,也會先清除無效對象,然后再根據需要擴容。
private void rehash() {
// 先清除無效 Entry
expungeStaleEntries();
// 判斷當前 table 中的條目數是否超出了閾值的 3/4
if (size >= threshold - threshold / 4)
resize();
}
清除無用對象和擴容的方法這里就不再展開說明了。
4.5 獲取 Entry 對象
取值是直接獲取到 Entry 對象,使用 getEntry(ThreadLocal key) 方法:
private Entry getEntry(ThreadLocal key) {
// 使用指定的 key 的 HashCode 計算索引位置
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取當前位置的 Entry
Entry e = table[i];
// 如果 Entry 不為 null 且 Entry 的 key 和 指定的 key 相等,則返回該 Entry
// 否則調用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
因為可能存在哈希沖突,key 對應的 Entry 的存儲位置可能不在通過 key 計算出的索引位置上,也就是說索引位置上的 Entry 不一定是 key 對應的 Entry。所以需要調用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法獲取。
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 索引位置上的 Entry 不為 null 進入循環,為 null 則返回 null
while (e != null) {
ThreadLocal k = e.get();
// 如果 Entry 的 key 和指定的 key 相等,則返回該 Entry
if (k == key)
return e;
// 如果 Entry 的 key 為 null (key 已經被回收了),清除無效的 Entry
// 否則獲取下一個位置的 Entry,循環判斷
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
4.6 移除指定的 Entry
private void remove(ThreadLocal key) {
Entry[] tab = table;
int len = tab.length;
// 使用指定的 key 的 HashCode 計算索引位置
int i = key.threadLocalHashCode & (len-1);
// 循環判斷索引位置的 Entry 是否為 null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 若 Entry 的 key 和指定的 key 相等,執行刪除操作
if (e.get() == key) {
// 清除 Entry 的 key 的引用
e.clear();
// 清除無效的 Entry
expungeStaleEntry(i);
return;
}
}
}
4.7 內存泄漏
在 ThreadLocalMap 的 set(),get() 和 remove() 方法中,都有清除無效 Entry 的操作,這樣做是為了降低內存泄漏發生的可能。
Entry 中的 key 使用了弱引用的方式,這樣做是為了降低內存泄漏發生的概率,但不能完全避免內存泄漏。
這句話的意思好象是矛盾的,下面來分析一下。
假設 Entry 的 key 沒有使用弱引用的方式,而是使用了強引用:由于 ThreadLocalMap 的生命周期和當前線程一樣長,那么當引用 ThreadLocal 的對象被回收后,由于 ThreadLocalMap 還持有 ThreadLocal 和對應 value 的強引用,ThreadLocal 和對應的 value 是不會被回收的,這就導致了內存泄漏。所以 Entry 以弱引用的方式避免了 ThreadLocal 沒有被回收而導致的內存泄漏,但是此時 value 仍然是無法回收的,依然會導致內存泄漏。
ThreadLocalMap 已經考慮到這種情況,并且有一些防護措施:在調用 ThreadLocal 的 get(),set() 和 remove() 的時候都會清除當前線程 ThreadLocalMap 中所有 key 為 null 的 value。這樣可以降低內存泄漏發生的概率。所以我們在使用 ThreadLocal 的時候,每次用完 ThreadLocal 都調用 remove() 方法,清除數據,防止內存泄漏。