在開始看原始碼之前,我們必須要知道ThreadLocal有什么作用:ThreadLocal 使同一個變數在不同執行緒間隔離,即每個執行緒都可以有自己獨立的副本,然后可以在該執行緒的方法間共享(隨時取出使用),不明白的話可以看文章最后一部分的使用示例,
這其實是一種空間換時間的思路,因為如果每個執行緒都有自己獨立的副本,就不用通過加鎖使執行緒串行化執行去保證執行緒安全了,節省了時間,但作為代價要為每個執行緒開辟一塊獨立的空間,
了解了 ThreadLocal 的功能后,那我們該如何設計ThreadLocal?
1.如何設計執行緒間隔離
首先,很容易想到每個執行緒都必須為 ThreadLocal 開辟一塊單獨的記憶體,但僅僅開辟一塊大小等于ThreadLocal的記憶體是不夠的的,因為一個執行緒可能有多個獨立的副本,換句話說就是可以在多個類中創建 ThreadLocal,比如:
public class A {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
public void f1(){ threadLocal1.set("test");
}
public class B {
private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
public void f2(){ threadLocal1.set(001);
}
那么對于執行的執行緒來說,test和001都是它的獨立副本,都要保存起來,而他倆的區別就在于具體ThreadLocal物件不同,
接下來,我們就看看在執行緒(Thread類)中到底是如何保存ThreadLocal的:

可以看到,每個Thread維護一個ThreadLocalMap,而存盤在ThreadLocalMap內的就是一個以Entry為元素的table陣列(Entry就是一個key-value結構:key為ThreadLocal,value為存盤的值),所以我們可以得到以下兩點資訊:
- 陣列保證了每個執行緒可以存盤多個獨立的副本
- Entry 提供了區分不同副本方式,即ThreadLocal不同
另外,雖然這里有兩個變數,但只有 threadLocals 是直接進行set/get操作的,若在父執行緒中創建子執行緒,會拷貝父執行緒的inheritableThreadLocals到子執行緒,
看原始碼前的要理解的邏輯終于說完了,下面進入正戲…
2.ThreadLocal
ThreadLocal 核心成員變數及主要建構式:
// ThreadLocal使用了泛型,所以可以存放任何型別
public class ThreadLocal<T> {
// 當前 ThreadLocal 的 hashCode,作用是計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
// nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
private final int threadLocalHashCode = nextHashCode();
// nextHashCode 直接決定 threadLocalHashCode(= nextHashCode++)
// 這么做因為ThreadLocal可能在不同類中new出來多個,但執行緒只有一個,若每次下標都從同一位置開始,雖然有hash碰撞處理策略,但仍然會影響效率
// static:保證了nextHashCode的唯一性,間接保證了threadHashCode唯一性
private static AtomicInteger nextHashCode = new AtomicInteger();
static class ThreadLocalMap{...}
// 只有空參構造
public ThreadLocal() {
}
// 計算 ThreadLocal 的 hashCode 值,就是通過CAS讓 nextHashCode++
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//......
}
2.1 set()
拿到當前執行緒的 threadLocals 并將 Entry(當前ThreadLocal物件,value)放入,另外,因為 set 操作每個執行緒都是串行的,所以不會有執行緒安全的問題
public void set(T value) {
// 拿到當前執行緒
Thread t = Thread.currentThread();
// 拿到當前執行緒的ThreadLocalMap,即threadLocals變數
ThreadLocalMap map = getMap(t);
// 當前 thradLocal 非空,即之前已經有獨立的副本資料了
if (map != null)
map.set(this, value); // 直接將當前 threadLocal和value傳入
// 當前threadLocal為空
else
createMap(t, value); // 初始化ThreadLocalMap
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
2.2 get()
在當前執行緒的 theadLocals 中獲取當前ThreadLocal物件對應的value
- 在當前執行緒拿到threadLocals
- 若threadLocals=null,則將其初始化
- 通過當前ThreadLocal物件獲取到相應Entry
- entry != null ,回傳result
- entry = null ,回傳null
public T get() {
// 拿出當前執行緒
Thread t = Thread.currentThread();
// 從執行緒中拿到 threadLocals(ThreadLocalMap)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 從 map 中拿到相應entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不為空,讀取當前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 否則給當前執行緒的 ThreadLocal 初始化,并回傳初始值 null
return setInitialValue();
}
3.ThreadLocalMap
- 雖然是內部類,但 ThreadLocalMap 不像 List 的 Node 是 List 的組成部分(List > Node)
- ThreadLocalMap是用來給Thread作為屬性,并保存ThreadLocal的 (Thread > ThreadLocalMap > ThreadLocal)
// 靜態內部類,可直接被外部呼叫
static class ThreadLocalMap {
// Entry(k,v)
// k = WeakReference 是弱參考,當沒有參考指向時,會直接被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
// 當前 ThreadLocal 關聯的值
Object value;
// WeakReference 的參考 referent 就是 ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 存盤 (ThreadLocal,Obj) 的陣列
private Entry[] table;
// 陣列的初始化大小
private static final int INITIAL_CAPACITY = 16;
// 擴容的閾值,默認是陣列大小的三分之二
private int threshold;
//.......
}
3.1 set()
將 Entry(threadLocal,Object Value)放入 threadLocals的陣列
- 獲取到 threadLocals 的陣列
- 計算當前ThreadLocal對應的陣列下標
- 將Entry(threadLocal,Object Value)放入陣列
- 無hash碰撞,new Entry放入
- 若出現hash碰撞,則i++,直到找到沒有Entry的位置,new Entry放入
- 若碰見key相同(ThreadLocal),則替換value
- 判斷是否需要擴容
private void set(ThreadLocal<?> key, Object value) {
// 1.拿到當前threadLocals的陣列
Entry[] tab = table;
int len = tab.length;
// 2.計算當前 ThreadLocal 在陣列中的下標,其實就是 ThreadLocal 的 hashCode 和陣列大小-1取余
int i = key.threadLocalHashCode & (len-1);
// 可以看到回圈的結束條件是 tab[i]==null,即無哈希沖突
// 若出現哈希沖突時,依次向后(i++)尋找空槽點,nextIndex方法就是讓在不超過陣列長度的基礎上,把陣列的索引位置 + 1
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到記憶體地址一樣的 ThreadLocal,直接替換
if (k == key) {
e.value = value;
return;
}
// 當前 key 是 null,說明 ThreadLocal 被清理了,直接替換掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 當前 i 位置是無值的,可以被當前 thradLocal 使用
tab[i] = new Entry(key, value);
int sz = ++size;
// 當陣列大小大于等于擴容閾值(陣列大小的三分之二)時,進行擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
3.2 getEntry()
獲取相應節點Entry
- 計算當前ThreadLocal對應的索引位置(hashcode 取模陣列大小-1 )
- 若 e != null,回傳當前Entry
- 若 e == null 或 有但key(ThreadLocal)不符,呼叫 getEntryAfterMiss 自旋進行尋找
private Entry getEntry(ThreadLocal<?> key) {
// 計算索引位置:ThreadLocal 的 hashCode 取模陣列大小-1
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// e 不為空 && e 的 ThreadLocal 的記憶體地址和 key 相同
if (e != null && e.get() == key)
return e; // 直接回傳
// 因為上面解決Hash沖突的方法是i++,所以會出現計算出的槽點為慷訓者不等于當前ThreadLocal的情況
else
return getEntryAfterMiss(key, i, e); // 繼續通過 getEntryAfterMiss 方法找
}
getEntryAfterMiss:根據 thradLocalMap set 時解決陣列索引位置沖突的邏輯,該方法的尋找邏輯也是對應的,即自旋 i+1,直到找到為止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 在大量使用不同 key 的 ThreadLocal 時,其實還蠻耗性能的
while (e != null) {
ThreadLocal<?> k = e.get();
// 記憶體地址一樣,表示找到了
if (k == key)
return e;
// 洗掉沒用的 key
if (k == null)
expungeStaleEntry(i);
// 繼續使索引位置 + 1
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
3.3 resize()
ThreadLocalMap 中的 ThreadLocal 的個數超過閾值時,ThreadLocalMap 就要開始擴容了
- 拿到threadLocals的table
- 初始化新陣列,大小為原來2倍
- 將老陣列拷貝到新陣列
- 根據key(ThreadLocal)計算新的索引位置
- 若出現hash碰撞,i++
- 計算新的擴容閾值,將新陣列賦給table
private void resize() {
// 1.拿出舊的陣列
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 2.計算新陣列的大小,為老陣列的兩倍
int newLen = oldLen * 2;
// 初始化新陣列
Entry[] newTab = new Entry[newLen];
int count = 0;
// 3.老陣列的值拷貝到新陣列上
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 {
// 計算 ThreadLocal 在新陣列中的位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果出現哈希沖突,即索引 h 的位置值不為空,往后+1,直到找到值為空的索引位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 給新陣列賦值
newTab[h] = e;
count++;
}
}
}
// 4.計算新陣列下次擴容閾值,為陣列長度的三分之二
setThreshold(newLen);
size = count;
table = newTab;
}
4.使用示例
下面啟動 9 個子執行緒,每個子執行緒都將名字(thread - i)存在同一個變數中,然后再列印出來,可以想到,如果同一個變數可以做到執行緒間隔離(互補影響),控制臺正確的結果應該是 thread - 0 到 thread - 8,
下面就分別演示演示這個變數的兩種實作:1.普通變數String ,2.ThreadLocal<String>
4.1 普通變數
public class StringTest {
// 保存執行緒名的普通變數value
private String value;
// 不直接設定value,而是暴露出get和set方法
private String getString() { return string; }
private void setString(String string) { this.string = string; }
public static void main(String[] args) {
StringTest test= new StringTest ();
int threads = 9; // 要啟動的執行緒個數
CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主執行緒在子執行緒未完成前結束
// 啟動9個子執行緒
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
test.setString(Thread.currentThread().getName()); // 向變數value中存入執行緒名 thread - i
System.out.println(test.getString()); // 然后列印出來,注:這里可能存在并發
countDownLatch.countDown(); // 門栓-1
}, "thread - " + i);
thread.start();
}
}
countDownLatch.await(); // 等countDownLatch為0時,主執行緒恢復運行
}
結果如下:
thread - 1
thread - 2
thread - 1
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8
可以看到沒有 thread - 0,反而 thread - 1 出現了兩次,所以使用普通型別的變數無法實作同一變數對于不同執行緒隔離
4.2 ThreadLocal
使用ThreadLocal時,一般宣告為static
- 一個類一個ThreadLocal–> 當前執行緒一個Entry 就夠了
- 使用時呼叫方便
public class ThreadLocalStringTest {
// 保存執行緒名的ThreadLocal變數threadLocal
// 注:這里除了是String,也可是別的任何型別(Integer,List,Map...)
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 不直接操作 threadLocal,而是封裝成 set/get 方法
private String getString() { return threadLocal.get(); }
private void setString(String string) { threadLocal.set(string);}
public static void main(String[] args) {
ThreadLocalStringTest test= new ThreadLocalStringTest();
int threads = 9; // 要創建的子執行緒個數
CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主執行緒在子執行緒未完成前結束
// 創建 9 個執行緒
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
test.setString(Thread.currentThread().getName()); // 向ThreadLocal中存入當前執行緒名 thread - i
System.out.println(test.getString()); // 向ThreadLocal獲取剛存的執行緒名,注:可能存在并發
countDownLatch.countDown(); // 門栓-1
}, "thread - " + i);
thread.start();
}
countDownLatch.await(); // 等countDownLatch為0時,主執行緒恢復運行
}
}
運行結果:
thread - 0
thread - 1
thread - 2
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8
可以看到運行結果符合預期,即ThreadLocal實作了同一變數在執行緒間隔離,另外ThreadLocal還可以用于單點登錄,用來保存不同請求執行緒的token,然后在決議時取出,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/152885.html
標籤:其他
