前言
ThreadLocal
很多同學都搞不懂是什麼東西,可以用來幹嘛。但面試時卻又經常問到,所以這次我和大家一起學習ThreadLocal
這個類。
下面我就以面試問答的形式學習我們的——ThreadLocal
類(原始碼分析基於JDK8)
本文同步釋出於簡書 :www.jianshu.com/p/807686414…
問答內容
1.
問:ThreadLocal
瞭解嗎?您能給我說說他的主要用途嗎?
答:
從JAVA官方對
ThreadLocal
類的說明定義(定義在示例程式碼中):ThreadLocal
類用來提供執行緒內部的區域性變數。這種變數在多執行緒環境下訪問(通過get
和set
方法訪問)時能保證各個執行緒的變數相對獨立於其他執行緒內的變數。ThreadLocal
例項通常來說都是private static
型別的,用於關聯執行緒和執行緒上下文。我們可以得知
ThreadLocal
的作用是:ThreadLocal
的作用是提供執行緒內的區域性變數,不同的執行緒之間不會相互干擾,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或元件之間一些公共變數的傳遞的複雜度。上述可以概述為:
ThreadLocal
提供執行緒內部的區域性變數,在本執行緒內隨時隨地可取,隔離其他執行緒。
示例程式碼:
/**
* 該類提供了執行緒區域性 (thread-local) 變數。 這些變數不同於它們的普通對應物,
* 因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數
* 它獨立於變數的初始化副本。ThreadLocal 例項通常是類中的 private static 欄位
* 它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。
*
* 例如,以下類生成對每個執行緒唯一的區域性識別符號。
*
* 執行緒 ID 是在第一次呼叫 UniqueThreadIdGenerator.getCurrentThreadId() 時分配的,
* 在後續呼叫中不會更改。
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // 原子性整數,包含下一個分配的執行緒Thread ID
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // 每一個執行緒對應的Thread ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // 返回當前執行緒對應的唯一Thread ID, 必要時會進行分配
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* 每個執行緒都保持對其執行緒區域性變數副本的隱式引用,只要執行緒是活動的並且 ThreadLocal 例項是可訪問的
* 線上程消失之後,其執行緒區域性例項的所有副本都會被垃圾回收,(除非存在對這些副本的其他引用)。
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
public class ThreadLocal<T> {
·····
/**
* 自定義雜湊碼(僅在ThreadLocalMaps中有用)
* 可用於降低hash衝突
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 生成下一個雜湊碼hashCode. 生成操作是原子性的. 從0開始
*
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 表示了連續分配的兩個ThreadLocal例項的threadLocalHashCode值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一個雜湊碼hashCode
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
·····
}複製程式碼
- 其中
nextHashCode()
方法就是一個原子類不停地去加上0x61c88647,這是一個很特別的數,叫斐波那契雜湊(Fibonacci Hashing),斐波那契又有一個名稱叫黃金分割,也就是說將這個數作為雜湊值的增量將會使雜湊表的分佈更為均勻。
2.
問:ThreadLocal
實現原理是什麼,它是怎麼樣做到區域性變數不同的執行緒之間不會相互干擾的?
答:
通常,如果我不去看原始碼的話,我猜
ThreadLocal
是這樣子設計的:每個ThreadLocal
類都建立一個Map
,然後用執行緒的IDthreadID
作為Map
的key
,要儲存的區域性變數作為Map
的value
,這樣就能達到各個執行緒的值隔離的效果。這是最簡單的設計方法,JDK最早期的ThreadLocal
就是這樣設計的。但是,JDK後面優化了設計方案,現時JDK8
ThreadLocal
的設計是:每個Thread
維護一個ThreadLocalMap
雜湊表,這個雜湊表的key
是ThreadLocal
例項本身,value
才是真正要儲存的值Object
。這個設計與我們一開始說的設計剛好相反,這樣設計有如下幾點優勢:
1) 這樣設計之後每個
Map
儲存的Entry
數量就會變小,因為之前的儲存數量由Thread
的數量決定,現在是由ThreadLocal
的數量決定。2) 當
Thread
銷燬之後,對應的ThreadLocalMap
也會隨之銷燬,能減少記憶體的使用。
上述解釋主要參考自:ThreadLocal和synchronized的區別?
3.
問:您能說說ThreadLocal
常用操作的底層實現原理嗎?如儲存set(T value)
,獲取get()
,刪除remove()
等操作。
答:
呼叫
get()
操作獲取ThreadLocal
中對應當前執行緒儲存的值時,進行了如下操作:1 ) 獲取當前執行緒
Thread
物件,進而獲取此執行緒物件中維護的ThreadLocalMap
物件。2 ) 判斷當前的
ThreadLocalMap
是否存在:- 如果存在,則以當前的
ThreadLocal
為key
,呼叫ThreadLocalMap
中的getEntry
方法獲取對應的儲存實體 e。找到對應的儲存實體 e,獲取儲存實體 e 對應的value
值,即為我們想要的當前執行緒對應此ThreadLocal
的值,返回結果值。 如果不存在,則證明此執行緒沒有維護的
ThreadLocalMap
物件,呼叫setInitialValue
方法進行初始化。返回setInitialValue
初始化的值。setInitialValue
方法的操作如下:1 ) 呼叫
initialValue
獲取初始化的值。2 ) 獲取當前執行緒
Thread
物件,進而獲取此執行緒物件中維護的ThreadLocalMap
物件。3 ) 判斷當前的
ThreadLocalMap
是否存在:如果存在,則呼叫
map.set
設定此實體entry
。如果不存在,則呼叫
createMap
進行ThreadLocalMap
物件的初始化,並將此實體entry
作為第一個值存放至ThreadLocalMap
中。
PS:關於ThreadLocalMap
對應的相關操作,放在下一個問題詳細說明。
示例程式碼:
/**
* 返回當前執行緒對應的ThreadLocal的初始值
* 此方法的第一次呼叫發生在,當執行緒通過{@link #get}方法訪問此執行緒的ThreadLocal值時
* 除非執行緒先呼叫了 {@link #set}方法,在這種情況下,
* {@code initialValue} 才不會被這個執行緒呼叫。
* 通常情況下,每個執行緒最多呼叫一次這個方法,
* 但也可能再次呼叫,發生在呼叫{@link #remove}方法後,
* 緊接著呼叫{@link #get}方法。
*
* <p>這個方法僅僅簡單的返回null {@code null};
* 如果程式設計師想ThreadLocal執行緒區域性變數有一個除null以外的初始值,
* 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
* 通常, 可以通過匿名內部類的方式實現
*
* @return 當前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
/**
* 建立一個ThreadLocal
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
/**
* 返回當前執行緒中儲存ThreadLocal的值
* 如果當前執行緒沒有此ThreadLocal變數,
* 則它會通過呼叫{@link #initialValue} 方法進行初始化值
*
* @return 返回當前執行緒對應此ThreadLocal的值
*/
public T get() {
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以當前的ThreadLocal 為 key,呼叫getEntry獲取對應的儲存實體e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到對應的儲存實體 e
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取儲存實體 e 對應的 value值
// 即為我們想要的當前執行緒對應此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在,則證明此執行緒沒有維護的ThreadLocalMap物件
// 呼叫setInitialValue進行初始化
return setInitialValue();
}
/**
* set的變樣實現,用於初始化值initialValue,
* 用於代替防止使用者重寫set()方法
*
* @return the initial value 初始化後的值
*/
private T setInitialValue() {
// 呼叫initialValue獲取初始化的值
T value = initialValue();
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則呼叫map.set設定此實體entry
map.set(this, value);
else
// 1)當前執行緒Thread 不存在ThreadLocalMap物件
// 2)則呼叫createMap進行ThreadLocalMap物件的初始化
// 3)並將此實體entry作為第一個值存放至ThreadLocalMap中
createMap(t, value);
// 返回設定的值value
return value;
}
/**
* 獲取當前執行緒Thread對應維護的ThreadLocalMap
*
* @param t the current thread 當前執行緒
* @return the map 對應維護的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}複製程式碼
呼叫
set(T value)
操作設定ThreadLocal中對應當前執行緒要儲存的值時,進行了如下操作:1 ) 獲取當前執行緒
Thread
物件,進而獲取此執行緒物件中維護的ThreadLocalMap
物件。2 ) 判斷當前的
ThreadLocalMap
是否存在:如果存在,則呼叫
map.set
設定此實體entry
。如果不存在,則呼叫
createMap
進行ThreadLocalMap
物件的初始化,並將此實體entry
作為第一個值存放至ThreadLocalMap
中。
示例程式碼:
/**
* 設定當前執行緒對應的ThreadLocal的值
* 大多數子類都不需要重寫此方法,
* 只需要重寫 {@link #initialValue}方法代替設定當前執行緒對應的ThreadLocal的值
*
* @param value 將要儲存在當前執行緒對應的ThreadLocal的值
*
*/
public void set(T value) {
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則呼叫map.set設定此實體entry
map.set(this, value);
else
// 1)當前執行緒Thread 不存在ThreadLocalMap物件
// 2)則呼叫createMap進行ThreadLocalMap物件的初始化
// 3)並將此實體entry作為第一個值存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 為當前執行緒Thread 建立對應維護的ThreadLocalMap.
*
* @param t the current thread 當前執行緒
* @param firstValue 第一個要存放的ThreadLocal變數值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}複製程式碼
呼叫
remove()
操作刪除ThreadLocal中對應當前執行緒已儲存的值時,進行了如下操作:1 ) 獲取當前執行緒
Thread
物件,進而獲取此執行緒物件中維護的ThreadLocalMap
物件。2 ) 判斷當前的
ThreadLocalMap
是否存在, 如果存在,則呼叫map.remove
,以當前ThreadLocal
為key
刪除對應的實體entry
。
- 示例程式碼:
/** * 刪除當前執行緒中儲存的ThreadLocal對應的實體entry * 如果此ThreadLocal變數在當前執行緒中呼叫 {@linkplain #get read}方法 * 則會通過呼叫{@link #initialValue}進行再次初始化, * 除非此值value是通過當前執行緒內建呼叫 {@linkplain #set set}設定的 * 這可能會導致在當前執行緒中多次呼叫{@code initialValue}方法 * * @since 1.5 */ public void remove() { // 獲取當前執行緒物件中維護的ThreadLocalMap物件 ThreadLocalMap m = getMap(Thread.currentThread()); // 如果此map存在 if (m != null) // 存在則呼叫map.remove // 以當前ThreadLocal為key刪除對應的實體entry m.remove(this); }複製程式碼
4.
問:對ThreadLocal
的常用操作實際是對執行緒Thread
中的ThreadLocalMap
進行操作,核心是ThreadLocalMap
這個雜湊表,你能談談ThreadLocalMap
的內部底層實現嗎?
答:
ThreadLocalMap
的底層實現是一個定製的自定義HashMap
雜湊表,核心組成元素有:1 )
Entry[] table;
:底層雜湊表 table, 必要時需要進行擴容,底層雜湊表 table.length 長度必須是2的n次方。2 )
int size;
:實際儲存鍵值對元素個數 entries3 )
int threshold;
:下一次擴容時的閾值,閾值 threshold = 底層雜湊表table的長度len * 2 / 3
。當size >= threshold
時,遍歷table
並刪除key
為null
的元素,如果刪除後size >= threshold*3/4
時,需要對table
進行擴容(詳情請檢視set(ThreadLocal<?> key, Object value)
方法說明)。其中
Entry[] table;
雜湊表儲存的核心元素是Entry
,Entry
包含:1 )
ThreadLocal<?> k;
:當前儲存的ThreadLocal
例項物件2 )
Object value;
:當前 ThreadLocal 對應儲存的值value需要注意的是,此
Entry
繼承了弱引用WeakReference
,所以在使用ThreadLocalMap
時,發現key == null
,則意味著此key ThreadLocal
不在被引用,需要將其從ThreadLocalMap
雜湊表中移除。(弱引用相關問題解釋請檢視 問答 5)
示例程式碼:
/**
* ThreadLocalMap 是一個定製的自定義 hashMap 雜湊表,只適合用於維護
* 執行緒對應ThreadLocal的值. 此類的方法沒有在ThreadLocal 類外部暴露,
* 此類是私有的,允許在 Thread 類中以欄位的形式宣告 ,
* 以助於處理儲存量大,生命週期長的使用用途,
* 此類定製的雜湊表實體鍵值對使用弱引用WeakReferences 作為key,
* 但是, 一旦引用不在被使用,
* 只有當雜湊表中的空間被耗盡時,對應不再使用的鍵值對實體才會確保被 移除回收。
*/
static class ThreadLocalMap {
/**
* 實體entries在此hash map中是繼承弱引用 WeakReference,
* 使用ThreadLocal 作為 key 鍵. 請注意,當key為null(i.e. entry.get()
* == null) 意味著此key不再被引用,此時實體entry 會從雜湊表中刪除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 當前 ThreadLocal 對應儲存的值value. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始容量大小 16 -- 必須是2的n次方.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 底層雜湊表 table, 必要時需要進行擴容.
* 底層雜湊表 table.length 長度必須是2的n次方.
*/
private Entry[] table;
/**
* 實際儲存鍵值對元素個數 entries.
*/
private int size = 0;
/**
* 下一次擴容時的閾值
*/
private int threshold; // 預設為 0
/**
* 設定觸發擴容時的閾值 threshold
* 閾值 threshold = 底層雜湊表table的長度 len * 2 / 3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 獲取該位置i對應的下一個位置index
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 獲取該位置i對應的上一個位置index
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}複製程式碼
ThreadLocalMap
的構造方法是延遲載入的,也就是說,只有當執行緒需要儲存對應的ThreadLocal
的值時,才初始化建立一次(僅初始化一次)。初始化步驟如下:1) 初始化底層陣列
table
的初始容量為 16。2) 獲取
ThreadLocal
中的threadLocalHashCode
,通過threadLocalHashCode & (INITIAL_CAPACITY - 1)
,即ThreadLocal 的 hash 值 threadLocalHashCode % 雜湊表的長度 length 的方式計算該實體的儲存位置。3) 儲存當前的實體,key 為 : 當前ThreadLocal value:真正要儲存的值
4)設定當前實際儲存元素個數 size 為 1
5)設定閾值
setThreshold(INITIAL_CAPACITY)
,為初始化容量 16 的 2/3。
示例程式碼:
/**
* 用於建立一個新的hash map包含 (firstKey, firstValue).
* ThreadLocalMaps 構造方法是延遲載入的,所以我們只會在至少有一個
* 實體entry存放時,才初始化建立一次(僅初始化一次)。
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 table 初始容量為 16
table = new Entry[INITIAL_CAPACITY];
// 計算當前entry的儲存位置
// 儲存位置計算等價於:
// ThreadLocal 的 hash 值 threadLocalHashCode % 雜湊表的長度 length
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 儲存當前的實體,key 為 : 當前ThreadLocal value:真正要儲存的值
table[i] = new Entry(firstKey, firstValue);
// 設定當前實際儲存元素個數 size 為 1
size = 1;
// 設定閾值,為初始化容量 16 的 2/3。
setThreshold(INITIAL_CAPACITY);
}複製程式碼
ThreadLocal
的get()
操作實際是呼叫ThreadLocalMap
的getEntry(ThreadLocal<?> key)
方法,此方法快速適用於獲取某一存在key
的實體entry
,否則,應該呼叫getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法獲取,這樣做是為了最大限制地提高直接命中的效能,該方法進行了如下操作:1 ) 計算要獲取的
entry
的儲存位置,儲存位置計算等價於:ThreadLocal
的hash
值threadLocalHashCode
% 雜湊表的長度length
。2 ) 根據計算的儲存位置,獲取到對應的實體
Entry
。判斷對應實體Entry
是否存在 並且key
是否相等:存在對應實體
Entry
並且對應key
相等,即同一ThreadLocal
,返回對應的實體Entry
。不存在對應實體
Entry
或者key
不相等,則通過呼叫getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法繼續查詢。getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法操作如下:1 ) 獲取底層雜湊表陣列
table
,迴圈遍歷對應要查詢的實體Entry
所關聯的位置。2 ) 獲取當前遍歷的
entry
的key ThreadLocal
,比較key
是否一致,一致則返回。3 ) 如果
key
不一致 並且key
為null
,則證明引用已經不存在,這是因為Entry
繼承的是WeakReference
,這是弱引用帶來的坑。呼叫expungeStaleEntry(int staleSlot)
方法刪除過期的實體Entry
(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明)。4 )
key
不一致 ,key
也不為空,則遍歷下一個位置,繼續查詢。5 ) 遍歷完畢,仍然找不到則返回
null
。
示例程式碼:
/**
* 根據key 獲取對應的實體 entry. 此方法快速適用於獲取某一存在key的
* 實體 entry,否則,應該呼叫getEntryAfterMiss方法獲取,這樣做是為
* 了最大限制地提高直接命中的效能
*
* @param key 當前thread local 物件
* @return the entry 對應key的 實體entry, 如果不存在,則返回null
*/
private Entry getEntry(ThreadLocal<?> key) {
// 計算要獲取的entry的儲存位置
// 儲存位置計算等價於:
// ThreadLocal 的 hash 值 threadLocalHashCode % 雜湊表
的長度 length
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取到對應的實體 Entry
Entry e = table[i];
// 存在對應實體並且對應key相等,即同一ThreadLocal
if (e != null && e.get() == key)
// 返回對應的實體Entry
return e;
else
// 不存在 或 key不一致,則通過呼叫getEntryAfterMiss繼續查詢
return getEntryAfterMiss(key, i, e);
}
/**
* 當根據key找不到對應的實體entry 時,呼叫此方法。
* 直接定位到對應的雜湊表位置
*
* @param key 當前thread local 物件
* @param i 此物件在雜湊表 table中的儲存位置 index
* @param e the entry 實體物件
* @return the entry 對應key的 實體entry, 如果不存在,則返回null
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 迴圈遍歷當前位置的所有實體entry
while (e != null) {
// 獲取當前entry 的 key ThreadLocal
ThreadLocal<?> k = e.get();
// 比較key是否一致,一致則返回
if (k == key)
return e;
// 找到對應的entry ,但其key 為 null,則證明引用已經不存在
// 這是因為Entry繼承的是WeakReference,這是弱引用帶來的坑
if (k == null)
// 刪除過期(stale)的entry
expungeStaleEntry(i);
else
// key不一致 ,key也不為空,則遍歷下一個位置,繼續查詢
i = nextIndex(i, len);
// 獲取下一個位置的實體 entry
e = tab[i];
}
// 遍歷完畢,找不到則返回null
return null;
}
/**
* 刪除對應位置的過期實體,並刪除此位置後對應相關聯位置key = null的實體
*
* @param staleSlot 已知的key = null 的對應的位置索引
* @return 對應過期實體位置索引的下一個key = null的位置
* (所有的對應位置都會被檢查)
*/
private int expungeStaleEntry(int staleSlot) {
// 獲取對應的底層雜湊表 table
Entry[] tab = table;
// 獲取雜湊表長度
int len = tab.length;
// 擦除這個位置上的髒資料
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 直到我們找到 Entry e = null,才執行rehash操作
// 就是遍歷完該位置的所有關聯位置的實體
Entry e;
int i;
// 查詢該位置對應所有關聯位置的過期實體,進行擦除操作
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 我們必須一直遍歷直到最後
// 因為還可能存在多個過期的實體
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* 刪除所有過期的實體
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}複製程式碼
ThreadLocal
的set(T value)
操作實際是呼叫ThreadLocalMap
的set(ThreadLocal<?> key, Object value)
方法,該方法進行了如下操作:1 ) 獲取對應的底層雜湊表
table
,計算對應threalocal
的儲存位置。2 ) 迴圈遍歷
table
對應該位置的實體,查詢對應的threadLocal
。3 ) 獲取當前位置的
threadLocal
,如果key threadLocal
一致,則證明找到對應的threadLocal
,將新值賦值給找到的當前實體Entry
的value
中,結束。4 ) 如果當前位置的
key threadLocal
不一致,並且key threadLocal
為null
,則呼叫replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)
方法(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明),替換該位置key == null
的實體為當前要設定的實體,結束。5 ) 如果當前位置的
key threadLocal
不一致,並且key threadLocal
不為null
,則建立新的實體,並存放至當前位置 itab[i] = new Entry(key, value);
,實際儲存鍵值對元素個數size + 1
,由於弱引用帶來了這個問題,所以要呼叫cleanSomeSlots(int i, int n)
方法清除無用資料(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明),才能判斷現在的size
有沒有達到閥值threshhold
,如果沒有要清除的資料,儲存元素個數仍然 大於 閾值 則呼叫rehash
方法進行擴容(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明)。
示例程式碼:
/**
* 設定對應ThreadLocal的值
*
* @param key 當前thread local 物件
* @param value 要設定的值
*/
private void set(ThreadLocal<?> key, Object value) {
// 我們不會像get()方法那樣使用快速設定的方式,
// 因為通常很少使用set()方法去建立新的實體
// 相對於替換一個已經存在的實體, 在這種情況下,
// 快速設定方案會經常失敗。
// 獲取對應的底層雜湊表 table
Entry[] tab = table;
// 獲取雜湊表長度
int len = tab.length;
// 計算對應threalocal的儲存位置
int i = key.threadLocalHashCode & (len-1);
// 迴圈遍歷table對應該位置的實體,查詢對應的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 獲取當前位置的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果key threadLocal一致,則證明找到對應的threadLocal
if (k == key) {
// 賦予新值
e.value = value;
// 結束
return;
}
// 如果當前位置的key threadLocal為null
if (k == null) {
// 替換該位置key == null 的實體為當前要設定的實體
replaceStaleEntry(key, value, i);
// 結束
return;
}
}
// 當前位置的k != key && k != null
// 建立新的實體,並存放至當前位置i
tab[i] = new Entry(key, value);
// 實際儲存鍵值對元素個數 + 1
int sz = ++size;
// 由於弱引用帶來了這個問題,所以先要清除無用資料,才能判斷現在的size有沒有達到閥值threshhold
// 如果沒有要清除的資料,儲存元素個數仍然 大於 閾值 則擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 擴容
rehash();
}
/**
* 當執行set操作時,獲取對應的key threadLocal,並替換過期的實體
* 將這個value值儲存在對應key threadLocal的實體中,無論是否已經存在體
* 對應的key threadLocal
*
* 有一個副作用, 此方法會刪除該位置下和該位置nextIndex對應的所有過期的實體
*
* @param key 當前thread local 物件
* @param value 當前thread local 物件對應儲存的值
* @param staleSlot 第一次找到此過期的實體對應的位置索引index
* .
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
// 獲取對應的底層雜湊表 table
Entry[] tab = table;
// 獲取雜湊表長度
int len = tab.length;
Entry e;
// 往前找,找到table中第一個過期的實體的下標
// 清理整個table是為了避免因為垃圾回收帶來的連續增長雜湊的危險
// 也就是說,雜湊表沒有清理乾淨,當GC到來的時候,後果很嚴重
// 記錄要清除的位置的起始首位置
int slotToExpunge = staleSlot;
// 從該位置開始,往前遍歷查詢第一個過期的實體的下標
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 找到key一致的ThreadLocal或找到一個key為 null的
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果我們找到了key,那麼我們就需要把它跟新的過期資料交換來保持雜湊表的順序
// 那麼剩下的過期Entry呢,就可以交給expungeStaleEntry方法來擦除掉
// 將新設定的實體放置在此過期的實體的位置上
if (k == key) {
// 替換,將要設定的值放在此過期的實體中
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果存在,則開始清除之前過期的實體
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 在這裡開始清除過期資料
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// / 如果我們沒有在往後查詢中找沒有找到過期的實體,
// 那麼slotToExpunge就是第一個過期Entry的下標了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 最後key仍沒有找到,則將要設定的新實體放置
// 在原過期的實體對應的位置上。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果該位置對應的其他關聯位置存在過期實體,則清除
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 啟發式的掃描查詢一些過期的實體並清除,
* 此方法會再新增新實體的時候被呼叫,
* 或者過期的元素被清除時也會被呼叫.
* 如果實在沒有過期資料,那麼這個演算法的時間複雜度就是O(log n)
* 如果有過期資料,那麼這個演算法的時間複雜度就是O(n)
*
* @param i 一個確定不是過期的實體的位置,從這個位置i開始掃描
*
* @param n 掃描控制: 有{@code log2(n)} 單元會被掃描,
* 除非找到了過期的實體, 在這種情況下
* 有{@code log2(table.length)-1} 的格外單元會被掃描.
* 當呼叫插入時, 這個引數的值是儲存實體的個數,
* 但如果呼叫 replaceStaleEntry方法, 這個值是雜湊表table的長度
* (注意: 所有的這些都可能或多或少的影響n的權重
* 但是這個版本簡單,快速,而且似乎執行效率還可以)
*
* @return true 返回true,如果有任何過期的實體被刪除。
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
/**
* 雜湊表擴容方法
* 首先掃描整個雜湊表table,刪除過期的實體
* 縮小雜湊表table大小 或 擴大雜湊表table大小,擴大的容量是加倍.
*/
private void rehash() {
// 刪除所有過期的實體
expungeStaleEntries();
// 使用較低的閾值threshold加倍以避免滯後
// 儲存實體個數 大於等於 閾值的3/4則擴容
if (size >= threshold - threshold / 4)
resize();
}
/**
* 擴容方法,以2倍的大小進行擴容
* 擴容的思想跟HashMap很相似,都是把容量擴大兩倍
* 不同之處還是因為WeakReference帶來的
*/
private void resize() {
// 記錄舊的雜湊表
Entry[] oldTab = table;
// 記錄舊的雜湊表長度
int oldLen = oldTab.length;
// 新的雜湊表長度為舊的雜湊表長度的2倍
int newLen = oldLen * 2;
// 建立新的雜湊表
Entry[] newTab = new Entry[newLen];
int count = 0;
// 逐一遍歷舊的雜湊表table的每個實體,重新分配至新的雜湊表中
for (int j = 0; j < oldLen; ++j) {
// 獲取對應位置的實體
Entry e = oldTab[j];
// 如果實體不會null
if (e != null) {
// 獲取實體對應的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果該ThreadLocal 為 null
if (k == null) {
// 則對應的值也要清除
// 就算是擴容,也不能忘了為擦除過期資料做準備
e.value = null; // Help the GC
} else {
// 如果不是過期實體,則根據新的長度重新計算儲存位置
int h = k.threadLocalHashCode & (newLen - 1);
// 將該實體儲存在對應ThreadLocal的最後一個位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 重新分配位置完畢,則重新計算閾值Threshold
setThreshold(newLen);
// 記錄實際儲存元素個數
size = count;
// 將新的雜湊表賦值至底層table
table = newTab;
}複製程式碼
ThreadLocal
的remove()
操作實際是呼叫ThreadLocalMap
的remove(ThreadLocal<?> key)
方法,該方法進行了如下操作:1 ) 獲取對應的底層雜湊表
table
,計算對應threalocal
的儲存位置。2 ) 迴圈遍歷
table
對應該位置的實體,查詢對應的threadLocal
。3 ) 獲取當前位置的
threadLocal
,如果key threadLocal
一致,則證明找到對應的threadLocal
,執行刪除操作,刪除此位置的實體,結束。
示例程式碼:
/**
* 移除對應ThreadLocal的實體
*/
private void remove(ThreadLocal<?> key) {
// 獲取對應的底層雜湊表 table
Entry[] tab = table;
// 獲取雜湊表長度
int len = tab.length;
// 計算對應threalocal的儲存位置
int i = key.threadLocalHashCode & (len-1);
// 迴圈遍歷table對應該位置的實體,查詢對應的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 如果key threadLocal一致,則證明找到對應的threadLocal
if (e.get() == key) {
// 執行清除操作
e.clear();
// 清除此位置的實體
expungeStaleEntry(i);
// 結束
return;
}
}
}複製程式碼
5.
問:ThreadLocalMap
中的儲存實體Entry
使用ThreadLocal
作為key
,但這個Entry
是繼承弱引用WeakReference
的,為什麼要這樣設計,使用了弱引用WeakReference
會造成記憶體洩露問題嗎?
答:
- 首先,回答這個問題之前,我需要解釋一下什麼是強引用,什麼是弱引用。
我們在正常情況下,普遍使用的是強引用:
A a = new A();
B b = new B();複製程式碼
當 a = null;b = null;
時,一段時間後,JAVA垃圾回收機制GC會將 a 和 b 對應所分配的記憶體空間給回收。
但考慮這樣一種情況:
C c = new C(b);
b = null;複製程式碼
當 b 被設定成null
時,那麼是否意味這一段時間後GC工作可以回收 b 所分配的記憶體空間呢?答案是否定的,因為即使 b 被設定成null
,但 c 仍然持有對 b 的引用,而且還是強引用,所以GC不會回收 b 原先所分配的空間,既不能回收,又不能使用,這就造成了 記憶體洩露。
那麼如何處理呢?
可以通過c = null;
,也可以使用弱引用WeakReference w = new WeakReference(b);
。因為使用了弱引用WeakReference
,GC是可以回收 b 原先所分配的空間的。
上述解釋主要參考自:對ThreadLocal實現原理的一點思考
- 回到
ThreadLocal
的層面上,ThreadLocalMap
使用ThreadLocal
的弱引用作為key
,如果一個ThreadLocal
沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal
勢必會被回收,這樣一來,ThreadLocalMap
中就會出現key
為null
的Entry
,就沒有辦法訪問這些key
為null
的Entry
的value
,如果當前執行緒再遲遲不結束的話,這些key
為null
的Entry
的value
就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成記憶體洩漏。
其實,ThreadLocalMap
的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal
的get()
,set()
,remove()
的時候都會清除執行緒ThreadLocalMap
裡所有key
為null
的value
。
但是這些被動的預防措施並不能保證不會記憶體洩漏:
使用
static
的ThreadLocal
,延長了ThreadLocal
的生命週期,可能導致的記憶體洩漏(參考ThreadLocal 記憶體洩露的例項分析)。分配使用了
ThreadLocal
又不再呼叫get()
,set()
,remove()
方法,那麼就會導致記憶體洩漏。
從表面上看記憶體洩漏的根源在於使用了弱引用。網上的文章大多著重分析ThreadLocal
使用了弱引用會導致記憶體洩漏,但是另一個問題也同樣值得思考:為什麼使用弱引用而不是強引用?
我們先來看看官方文件的說法:
To help deal with very large and long-lived usages,
the hash table entries use WeakReferences for keys.複製程式碼
為了應對非常大和長時間的用途,雜湊表使用弱引用的 key
。
下面我們分兩種情況討論:
key
使用強引用:引用的ThreadLocal
的物件被回收了,但是ThreadLocalMap
還持有ThreadLocal
的強引用,如果沒有手動刪除,ThreadLocal
不會被回收,導致Entry
記憶體洩漏。key
使用弱引用:引用的ThreadLocal
的物件被回收了,由於ThreadLocalMap
持有ThreadLocal
的弱引用,即使沒有手動刪除,ThreadLocal
也會被回收。value
在下一次ThreadLocalMap
呼叫get()
,set()
,remove()
的時候會被清除。比較兩種情況,我們可以發現:由於
ThreadLocalMap
的生命週期跟Thread
一樣長,如果都沒有手動刪除對應key
,都會導致記憶體洩漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal
不會記憶體洩漏,對應的value
在下一次ThreadLocalMap
呼叫get()
,set()
,remove()
的時候會被清除。
因此,ThreadLocal
記憶體洩漏的根源是:由於ThreadLocalMap
的生命週期跟Thread
一樣長,如果沒有手動刪除對應key
就會導致記憶體洩漏,而不是因為弱引用。
綜合上面的分析,我們可以理解ThreadLocal
記憶體洩漏的前因後果,那麼怎麼避免記憶體洩漏呢?
每次使用完ThreadLocal
,都呼叫它的remove()
方法,清除資料。
在使用執行緒池的情況下,沒有及時清理ThreadLocal
,不僅是記憶體洩漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal
就跟加鎖完要解鎖一樣,用完就清理。
上述解釋主要參考自:深入分析 ThreadLocal 記憶體洩漏問題
6.
問:ThreadLocal
和synchronized
的區別?
答:ThreadLocal
和synchronized
關鍵字都用於處理多執行緒併發訪問變數的問題,只是二者處理問題的角度和思路不同。
ThreadLocal
是一個Java類,通過對當前執行緒中的區域性變數的操作來解決不同執行緒的變數訪問的衝突問題。所以,ThreadLocal
提供了執行緒安全的共享物件機制,每個執行緒都擁有其副本。Java中的
synchronized
是一個保留字,它依靠JVM的鎖機制來實現臨界區的函式或者變數的訪問中的原子性。在同步機制中,通過物件的鎖機制保證同一時間只有一個執行緒訪問變數。此時,被用作“鎖機制”的變數時多個執行緒共享的。
- 同步機制(
synchronized
關鍵字)採用了以“時間換空間”的方式,提供一份變數,讓不同的執行緒排隊訪問。而ThreadLocal
採用了“以空間換時間”的方式,為每一個執行緒都提供一份變數的副本,從而實現同時訪問而互不影響。
7.
問:ThreadLocal
在現時有什麼應用場景?
答:總的來說ThreadLocal
主要是解決2種型別的問題:
解決併發問題:使用
ThreadLocal
代替synchronized
來保證執行緒安全。同步機制採用了“以時間換空間”的方式,而ThreadLoca
l採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。解決資料儲存問題:
ThreadLocal
為變數在每個執行緒中都建立了一個副本,所以每個執行緒可以訪問自己內部的副本變數,不同執行緒之間不會互相干擾。如一個Parameter
物件的資料需要在多個模組中使用,如果採用引數傳遞的方式,顯然會增加模組之間的耦合性。此時我們可以使用ThreadLocal
解決。
應用場景:
Spring
使用ThreadLocal
解決執行緒安全問題
我們知道在一般情況下,只有無狀態的
Bean
才可以在多執行緒環境下共享,在Spring
中,絕大部分Bean
都可以宣告為singleton
作用域。就是因為Spring
對一些Bean
(如RequestContextHolder
、TransactionSynchronizationManager
、LocaleContextHolder
等)中非執行緒安全狀態採用ThreadLocal
進行處理,讓它們也成為執行緒安全的狀態,因為有狀態的Bean
就可以在多執行緒中共享了。一般的
Web
應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過介面向上層開放功能呼叫。在一般情況下,從接收請求到返回響應所經過的所有程式呼叫都同屬於一個執行緒ThreadLocal
是解決執行緒安全問題一個很好的思路,它通過為每個執行緒提供一個獨立的變數副本解決了變數併發訪問的衝突問題。在很多情況下,ThreadLocal
比直接使用synchronized
同步機制解決執行緒安全問題更簡單,更方便,且結果程式擁有更高的併發性。
示例程式碼:
public abstract class RequestContextHolder {
····
private static final boolean jsfPresent =
ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<RequestAttributes>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<RequestAttributes>("Request context");
·····
}複製程式碼
總結
ThreadLocal
提供執行緒內部的區域性變數,在本執行緒內隨時隨地可取,隔離其他執行緒。ThreadLocal
的設計是:每個Thread
維護一個ThreadLocalMap
雜湊表,這個雜湊表的key
是ThreadLocal
例項本身,value
才是真正要儲存的值Object
。對
ThreadLocal
的常用操作實際是對執行緒Thread
中的ThreadLocalMap
進行操作。ThreadLocalMap
的底層實現是一個定製的自定義HashMap
雜湊表,ThreadLocalMap
的閾值threshold
= 底層雜湊表table
的長度len * 2 / 3
,當實際儲存元素個數size
大於或等於 閾值threshold
的3/4
時size >= threshold*3/4
,則對底層雜湊表陣列table
進行擴容操作。ThreadLocalMap
中的雜湊表Entry[] table
儲存的核心元素是Entry
,儲存的key
是ThreadLocal
例項物件,value
是ThreadLocal
對應儲存的值value
。需要注意的是,此Entry
繼承了弱引用WeakReference
,所以在使用ThreadLocalMap
時,發現key == null
,則意味著此key ThreadLocal
不在被引用,需要將其從ThreadLocalMap
雜湊表中移除。ThreadLocalMap
使用ThreadLocal
的弱引用作為key
,如果一個ThreadLocal
沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal
勢必會被回收。所以,在ThreadLocal
的get()
,set()
,remove()
的時候都會清除執行緒ThreadLocalMap
裡所有key
為null
的value
。如果我們不主動呼叫上述操作,則會導致記憶體洩露。為了安全地使用
ThreadLocal
,必須要像每次使用完鎖就解鎖一樣,在每次使用完ThreadLocal
後都要呼叫remove()
來清理無用的Entry
。這在操作在使用執行緒池時尤為重要。ThreadLocal
和synchronized
的區別:同步機制(synchronized
關鍵字)採用了以“時間換空間”的方式,提供一份變數,讓不同的執行緒排隊訪問。而ThreadLocal
採用了“以空間換時間”的方式,為每一個執行緒都提供一份變數的副本,從而實現同時訪問而互不影響。ThreadLocal
主要是解決2種型別的問題:A. 解決併發問題:使用ThreadLocal
代替同步機制解決併發問題。B. 解決資料儲存問題:如一個Parameter
物件的資料需要在多個模組中使用,如果採用引數傳遞的方式,顯然會增加模組之間的耦合性。此時我們可以使用ThreadLocal
解決。
參考文章
深入淺出ThreadLocal
ThreadLocal和synchronized的區別?
深入剖析ThreadLocal
ThreadLocal內部機制
聊一聊Spring中的執行緒安全性
對ThreadLocal實現原理的一點思考
深入分析 ThreadLocal 記憶體洩漏問題
學習Spring必學的Java基礎知識(6)----ThreadLocal
ThreadLocal設計模式
ThreadLocal案例分析
Spring單例模式與執行緒安全ThreadLocal