ThreadLocal 用於提供線程局部變量,在多線程環境能夠保證各個線程裏的變量獨立於其它線程裏的變量。也就是說 ThreadLocal 能夠爲每一個線程建立一個【單獨的變量副本】,至關於線程的 private static 類型變量。java
ThreadLocal 的做用和同步機制有些相反:同步機制是爲了保證多線程環境下數據的一致性;而 ThreadLocal 是保證了多線程環境下數據的獨立性。數組
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());
}
}複製代碼
運行結果:bash
strLabel = child
threadLabel = main複製代碼
從運行結果能夠看出,對於 ThreadLocal 類型的變量,在一個線程中設置值,不影響其在其它線程中的值。也就是說 ThreadLocal 類型的變量的值在每一個線程中是獨立的。多線程
ThreadLocal 是怎樣保證其值在各個線程中是獨立的呢?下面分析下 ThreadLocal 的實現。ide
ThreadLocal 是構造函數只是一個簡單的無參構造函數,而且沒有任何實現。函數
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。this
ThreadLocalMap 至關於一個 HashMap,是真正保存值的地方。spa
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 中。線程
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 的初始值。code
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}複製代碼
ThreadLocal 還有一個 remove() 方法,用來移除當前 ThreadLocal 對應的值。一樣也是同過當前線程的 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 類型的變量在每一個線程中是獨立的,在多線程環境下不會相互影響。
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);
}複製代碼
構造方法中會新建一個數組,並將將第一次須要保存的鍵值存儲到一個數組中,完成一些初始化工做。
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 的一個內部類。
Entry 用於保存一個鍵值對,其中 key 以弱引用的方式保存:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}複製代碼
調用 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();
}複製代碼
清除無用對象和擴容的方法這裏就再也不展開說明了。
取值是直接獲取到 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;
}複製代碼
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;
}
}
}複製代碼
在 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() 方法,清除數據,防止內存泄漏。