本篇總結ThreadLocal的相關知識,它在實際開發中使用頻率較高,要認真學 ~
1 ThreadLocal
- ThreadLocal是一種類型,用於提供 線程局部變量,在多線程環境下可以 保證各個線程裏的變量獨立於其他線程的變量。
- 即:ThreadLocal可爲每個線程創建一個單獨的變量副本,相當於線程的private static類型變量。
- 在多線程環境下,同步機制是爲了保證數據的一致性,而ThreadLocal是爲了保證數據的獨立性。
2 ThreadLocal實現
public void set(T value)
public T get()
public void remove()
protected T initialValue()
-
set(T value)方法
- 首先獲取當前線程,然後再獲取到當前線程的ThreadLocalMap,若它不爲null,則將value保存其中,並用當前的ThreadLocal作爲key;否則創建一個ThreadLocalMap並給到當前線程,然後保存value。
- ThreadLocalMap相當於一個HashMap,是真正保存值的地方。
-
get()方法
- 在 get() 方法中也會獲取到當前線程的 ThreadLocalMap,如果 ThreadLocalMap 不爲 null,則獲取 key 爲當前 ThreadLocal 的值;
- 否則調用 setInitialValue() 方法返回初始值,並保存到新創建的 ThreadLocalMap 中。
-
remove()方法
- 刪除當前key
3 ThreadLocalMap
-
在 set,get,initialValue 和 remove 方法中都會獲取到當前線程,然後通過當前線程獲取到 ThreadLocalMap,如果ThreadLocalMap 爲 null,則會創建一個 ThreadLocalMap,並給到當前線程。
-
可見,每個線程都會持有一個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;
// 存儲數據的哈希表
//這個table是一個Entry類型的數組,Entry是ThreadLocalMap的一個內部類。
//Entry 用於保存一個鍵值對,其中key以“弱引用”的方式保存。
private Entry[] table;
// table 中已存儲的條目數
private int size = 0;
// 表示一個閾值,當 table 中存儲的對象達到該值時就會擴容
private int threshold;
// 設置 threshold 的值
private void setThreshold(int len){
threshold = len *2/3;
}
-
保存鍵值對
調用
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 值。
-
在保存數據時,如果索引位置有 Entry,且該 Entry 的 key 爲 null,就會清除無效 Entry 來騰出空間,因爲:Entry的 key 是弱引用,key 一旦被回收(即 key 爲 null),就無法再訪問到 key 對應的 value。
-
在調整 table 容量時,也會先清除無效對象,然後再根據需要擴容。
-
獲取Entry對象
- 使用
getEntry(ThreadLocal key)
方法。 - 因爲可能存在哈希衝突,key對應的Entry的存儲位置可能不在由key計算出的索引位置上,所以要調用
getEntryAfterMiss(ThreadLocal key,int i,Entry e)
方法獲取。
- 使用
-
內存泄露
- 在ThreadLocalMap的set、get、remove方法中都有清除無效Entry的操作,和Entry的key使用弱引用的方式,都是爲了降低內存泄漏的概率,但不能完全避免。
- 開發中每次用完ThreadLocal 都要調用remove方法,清除所有key爲null的value,防止內存泄漏。