ThreadLocal
不少同窗都搞不懂是什麼東西,能夠用來幹嗎。但面試時卻又常常問到,因此此次我和你們一塊兒學習ThreadLocal
這個類。java
下面我就以面試問答的形式學習咱們的——ThreadLocal
類(源碼分析基於JDK8)程序員
本文同步發佈於簡書 :www.jianshu.com/p/807686414…面試
問:ThreadLocal
瞭解嗎?您能給我說說他的主要用途嗎?算法
答:設計模式
從JAVA官方對ThreadLocal
類的說明定義(定義在示例代碼中):ThreadLocal
類用來提供線程內部的局部變量。這種變量在多線程環境下訪問(經過get
和set
方法訪問)時能保證各個線程的變量相對獨立於其餘線程內的變量。ThreadLocal
實例一般來講都是private static
類型的,用於關聯線程和線程上下文。數組
咱們能夠得知ThreadLocal
的做用是:ThreadLocal
的做用是提供線程內的局部變量,不一樣的線程之間不會相互干擾,這種變量在線程的生命週期內起做用,減小同一個線程內多個函數或組件之間一些公共變量的傳遞的複雜度。安全
上述能夠概述爲:ThreadLocal
提供線程內部的局部變量,在本線程內隨時隨地可取,隔離其餘線程。bash
示例代碼:多線程
/**
* 該類提供了線程局部 (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),斐波那契又有一個名稱叫黃金分割,也就是說將這個數做爲哈希值的增量將會使哈希表的分佈更爲均勻。問:ThreadLocal
實現原理是什麼,它是怎麼樣作到局部變量不一樣的線程之間不會相互干擾的?併發
答:
一般,若是我不去看源代碼的話,我猜ThreadLocal
是這樣子設計的:每一個ThreadLocal
類都建立一個Map
,而後用線程的ID threadID
做爲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的區別?
問:您能說說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);
}複製代碼
ThreadLocal
的經常使用操做實際是對線程Thread
中的ThreadLocalMap
進行操做,核心是ThreadLocalMap
這個哈希表,你能談談ThreadLocalMap
的內部底層實現嗎?答:
ThreadLocalMap
的底層實現是一個定製的自定義HashMap
哈希表,核心組成元素有:
1 ) Entry[] table;
:底層哈希表 table, 必要時須要進行擴容,底層哈希表 table.length 長度必須是2的n次方。
2 ) int size;
:實際存儲鍵值對元素個數 entries
3 ) 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
,則建立新的實體,並存放至當前位置 i tab[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;
}
}
}複製代碼
問: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 內存泄漏問題
問:ThreadLocal
和synchronized
的區別?
答:ThreadLocal
和synchronized
關鍵字都用於處理多線程併發訪問變量的問題,只是兩者處理問題的角度和思路不一樣。
ThreadLocal
是一個Java類,經過對當前線程中的局部變量的操做來解決不一樣線程的變量訪問的衝突問題。因此,ThreadLocal
提供了線程安全的共享對象機制,每一個線程都擁有其副本。
Java中的synchronized
是一個保留字,它依靠JVM的鎖機制來實現臨界區的函數或者變量的訪問中的原子性。在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量。此時,被用做「鎖機制」的變量時多個線程共享的。
synchronized
關鍵字)採用了以「時間換空間」的方式,提供一份變量,讓不一樣的線程排隊訪問。而ThreadLocal
採用了「以空間換時間」的方式,爲每個線程都提供一份變量的副本,從而實現同時訪問而互不影響。問: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