前言
ThreadLocal為變數在每個執行緒中都建立了一個副本,所以每個執行緒可以訪問自己內部的副本變數,不同執行緒之間不會互相干擾。本文會基於實際場景介紹ThreadLocal如何使用以及內部實現機制。
應用場景
最近的一個web專案中,由於Parameter物件的資料需要在多個模組中使用,如果採用引數傳遞的方式,顯然會增加模組之間的耦合性。先看看用ThreadLocal是如何實現模組間共享資料的。
1 2 3 4 5 6 7 8 9 10 |
class Parameter { private static ThreadLocal<Parameter> _parameter= new ThreadLocal<>(); public static Parameter init() { _parameter.set(new Parameter()); } public static Parameter get() { _parameter.get(); } ...省略變數宣告 } |
- 在模組A中通過Parameter.init初始化。
- 在模組B或模組C中通過Parameter.get方法可以獲得同一執行緒中模組A已經初始化的Parameter物件。
那麼,在什麼場景下比較適合使用ThreadLocal?stackoverflow上有人給出了還不錯的回答。
When and how should I use a ThreadLocal variable?
One possible (and common) use is when you have some object that is not thread-safe, but you want to avoid synchronizing access to that object (I’m looking at you, SimpleDateFormat). Instead, give each thread its own instance of the object.
實現原理
從執行緒Thread的角度來看,每個執行緒內部都會持有一個對ThreadLocalMap例項的引用,ThreadLocalMap例項相當於執行緒的區域性變數空間,儲存著執行緒各自的資料,具體如下:
Entry
Entry繼承自WeakReference類,是儲存執行緒私有變數的資料結構。ThreadLocal例項作為引用,意味著如果ThreadLocal例項為null,就可以從table中刪除對應的Entry。
1 2 3 4 5 6 7 |
class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } |
ThreadLocalMap
內部使用table陣列儲存Entry,預設大小INITIAL_CAPACITY(16),先介紹幾個引數:
- size:table中元素的數量。
- threshold:table大小的2/3,當size >= threshold時,遍歷table並刪除key為null的元素,如果刪除後size >= threshold*3/4時,需要對table進行擴容。
ThreadLocal.set() 實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
private void set(ThreadLocal<?> key, Object value) { 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(); } |
從上面程式碼中看出來:
- 從當前執行緒Thread中獲取ThreadLocalMap例項。
- ThreadLocal例項和value封裝成Entry。
接下去看看Entry存入table陣列如何實現的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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; } |
- 通過ThreadLocal的nextHashCode方法生成hash值。
1234private static AtomicInteger nextHashCode = new AtomicInteger();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}
從nextHashCode方法可以看出,ThreadLocal每例項化一次,其hash值就原子增加HASH_INCREMENT。 - 通過 hash & (len -1) 定位到table的位置i,假設table中i位置的元素為f。
- 如果f != null,假設f中的引用為k:
- 如果k和當前ThreadLocal例項一致,則修改value值,返回。
- 如果k為null,說明這個f已經是stale(陳舊的)的元素。呼叫replaceStaleEntry方法刪除table中所有陳舊的元素(即entry的引用為null)並插入新元素,返回。
- 否則通過nextIndex方法找到下一個元素f,繼續進行步驟3。
- 如果f == null,則把Entry加入到table的i位置中。
- 通過cleanSomeSlots刪除陳舊的元素,如果table中沒有元素刪除,需判斷當前情況下是否要進行擴容。
table擴容
如果table中的元素數量達到閾值threshold的3/4,會進行擴容操作,過程很簡單:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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; } |
- 新建新的陣列newTab,大小為原來的2倍。
- 複製table的元素到newTab,忽略陳舊的元素,假設table中的元素e需要複製到newTab的i位置,如果i位置存在元素,則找下一個空位置進行插入。
ThreadLocal.get() 實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 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); } |
獲取當前的執行緒的threadLocals。
- 如果threadLocals不為null,則通過ThreadLocalMap.getEntry方法找到對應的entry,如果其引用和當前key一致,則直接返回,否則在table剩下的元素中繼續匹配。
- 如果threadLocals為null,則通過setInitialValue方法初始化,並返回。
123456789101112131415private 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);elsei = nextIndex(i, len);e = tab[i];}return null;}
總結
希望通過本文的介紹,大家可以對ThreadLocal有一個更加直觀清晰的認識,而不是隻見葉子,不見森林。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!