前言
ThreadLocal很多同學(xué)都搞不懂是什么東西,可以用來干嘛骄蝇。但面試時卻又經(jīng)常問到巡扇,所以這次我和大家一起學(xué)習(xí)ThreadLocal這個類坪圾。
下面我就以面試問答的形式學(xué)習(xí)我們的——ThreadLocal類(源碼分析基于JDK8)
問答內(nèi)容
- 問:ThreadLocal了解嗎?您能給我說說他的主要用途嗎杰赛?
答:
首先呢簸,ThreadLocal是用在多線程的場景的!7ν汀根时!如果僅僅就一個線程,那么不用談ThreadLocal了辰晕。
ThreadLocal歸納下來就2類用途:
- 保存線程上下文信息蛤迎,在任意需要的地方可以獲取:选L骜伞!
- 線程安全的窘问,避免某些情況需要考慮線程安全必須同步帶來的性能損失A就!惠赫!
保存線程上下文信息胸遇,在任意需要的地方可以獲取:盒巍V侥鳌!
由于ThreadLocal的特性概疆,同一線程在某地方進(jìn)行設(shè)置逗威,在隨后的任意地方都可以獲取到。從而可以用來保存線程上下文信息岔冀。
還有比如Spring的事務(wù)管理凯旭,用ThreadLocal存儲Connection,從而各個DAO可以獲取同一Connection使套,可以進(jìn)行事務(wù)回滾罐呼,提交等操作。
備注:ThreadLocal的這種用處侦高,很多時候是用在一些優(yōu)秀的框架里面的嫉柴,一般我們很少接觸,反而下面的場景我們接觸的更多一些奉呛!
線程安全的计螺,避免某些情況需要考慮線程安全必須同步帶來的性能損失:痪 !登馒!
ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路匙握。但是ThreadLocal也有局限性,我們來看看阿里規(guī)范:
每個線程往ThreadLocal中讀寫數(shù)據(jù)是線程隔離陈轿,互相之間不會影響的圈纺,所以ThreadLocal無法解決共享對象的更新問題!
由于不需要共享信息麦射,自然就不存在競爭問題了赠堵,從而保證了某些情況下線程的安全,以及避免了某些情況需要考慮線程安全必須同步帶來的性能損失7ㄈ臁C0取!
這類場景阿里規(guī)范里面也提到了:
示例代碼:
/**
* 該類提供了線程局部 (thread-local) 變量半等。 這些變量不同于它們的普通對應(yīng)物揍愁,
* 因為訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量
* 它獨立于變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段
* 它們希望將狀態(tài)與某一個線程(例如杀饵,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)莽囤。
*
* 例如,以下類生成對每個線程唯一的局部標(biāo)識符切距。
*
* 線程 ID 是在第一次調(diào)用 UniqueThreadIdGenerator.getCurrentThreadId() 時分配的朽缎,
* 在后續(xù)調(diào)用中不會更改。
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // 原子性整數(shù)谜悟,包含下一個分配的線程Thread ID
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // 每一個線程對應(yīng)的Thread ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // 返回當(dāng)前線程對應(yīng)的唯一Thread ID, 必要時會進(jìn)行分配
* public static int get() {
* return threadId.get();
* }
* }
*
* 每個線程都保持對其線程局部變量副本的隱式引用话肖,只要線程是活動的并且 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();
/**
* 表示了連續(xù)分配的兩個ThreadLocal實例的threadLocalHashCode值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一個哈希碼hashCode
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
- 其中nextHashCode()方法就是一個原子類不停地去加上0x61c88647,這是一個很特別的數(shù)蔚叨,叫斐波那契散列(Fibonacci Hashing)床蜘,斐波那契又有一個名稱叫黃金分割,也就是說將這個數(shù)作為哈希值的增量將會使哈希表的分布更為均勻蔑水。
- 問:ThreadLocal實現(xiàn)原理是什么邢锯,它是怎么樣做到局部變量不同的線程之間不會相互干擾的?
答:
每個Thread維護一個ThreadLocalMap哈希表搀别,這個哈希表的key是ThreadLocal實例本身丹擎,value才是真正要存儲的值Object。
這樣設(shè)計领曼,當(dāng)Thread銷毀之后鸥鹉,對應(yīng)的ThreadLocalMap也會隨之銷毀蛮穿,能減少內(nèi)存的使用庶骄。
- 問:您能說說ThreadLocal常用操作的底層實現(xiàn)原理嗎毁渗?如存儲set(T value),獲取get()单刁,刪除remove()等操作灸异。
答:
調(diào)用get()操作獲取ThreadLocal中對應(yīng)當(dāng)前線程存儲的值時,進(jìn)行了如下操作:
- 獲取當(dāng)前線程Thread對象羔飞,進(jìn)而獲取此線程對象中維護的ThreadLocalMap對象肺樟。
- 判斷當(dāng)前的ThreadLocalMap是否存在:
如果存在,則以當(dāng)前的ThreadLocal 為 key逻淌,調(diào)用ThreadLocalMap中的getEntry方法獲取對應(yīng)的存儲實體 e么伯。找到對應(yīng)的存儲實體 e,獲取存儲實體 e 對應(yīng)的 value值卡儒,即為我們想要的當(dāng)前線程對應(yīng)此ThreadLocal的值田柔,返回結(jié)果值。
如果不存在骨望,則證明此線程沒有維護的ThreadLocalMap對象硬爆,調(diào)用setInitialValue方法進(jìn)行初始化。返回setInitialValue初始化的值擎鸠。
setInitialValue方法的操作如下:
- 調(diào)用initialValue獲取初始化的值缀磕。
- 獲取當(dāng)前線程Thread對象,進(jìn)而獲取此線程對象中維護的ThreadLocalMap對象劣光。
- 判斷當(dāng)前的ThreadLocalMap是否存在:
- 如果存在袜蚕,則調(diào)用map.set設(shè)置此實體entry。
- 如果不存在绢涡,則調(diào)用createMap進(jìn)行ThreadLocalMap對象的初始化廷没,并將此實體entry作為第一個值存放至ThreadLocalMap中。
PS:關(guān)于ThreadLocalMap對應(yīng)的相關(guān)操作垂寥,放在下一個問題詳細(xì)說明颠黎。
示例代碼:
/**
* 返回當(dāng)前線程對應(yīng)的ThreadLocal的初始值
* 此方法的第一次調(diào)用發(fā)生在,當(dāng)線程通過{@link #get}方法訪問此線程的ThreadLocal值時
* 除非線程先調(diào)用了 {@link #set}方法滞项,在這種情況下狭归,
* {@code initialValue} 才不會被這個線程調(diào)用。
* 通常情況下文判,每個線程最多調(diào)用一次這個方法过椎,
* 但也可能再次調(diào)用,發(fā)生在調(diào)用{@link #remove}方法后戏仓,
* 緊接著調(diào)用{@link #get}方法疚宇。
*
* <p>這個方法僅僅簡單的返回null {@code null};
* 如果程序員想ThreadLocal線程局部變量有一個除null以外的初始值亡鼠,
* 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
* 通常, 可以通過匿名內(nèi)部類的方式實現(xiàn)
*
* @return 當(dāng)前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
/**
* 創(chuàng)建一個ThreadLocal
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
/**
* 返回當(dāng)前線程中保存ThreadLocal的值
* 如果當(dāng)前線程沒有此ThreadLocal變量,
* 則它會通過調(diào)用{@link #initialValue} 方法進(jìn)行初始化值
*
* @return 返回當(dāng)前線程對應(yīng)此ThreadLocal的值
*/
public T get() {
// 獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 獲取此線程對象中維護的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以當(dāng)前的ThreadLocal 為 key敷待,調(diào)用getEntry獲取對應(yīng)的存儲實體e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到對應(yīng)的存儲實體 e
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取存儲實體 e 對應(yīng)的 value值
// 即為我們想要的當(dāng)前線程對應(yīng)此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在间涵,則證明此線程沒有維護的ThreadLocalMap對象
// 調(diào)用setInitialValue進(jìn)行初始化
return setInitialValue();
}
/**
* set的變樣實現(xiàn),用于初始化值initialValue榜揖,
* 用于代替防止用戶重寫set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 調(diào)用initialValue獲取初始化的值
T value = initialValue();
// 獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 獲取此線程對象中維護的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則調(diào)用map.set設(shè)置此實體entry
map.set(this, value);
else
// 1)當(dāng)前線程Thread 不存在ThreadLocalMap對象
// 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對象的初始化
// 3)并將此實體entry作為第一個值存放至ThreadLocalMap中
createMap(t, value);
// 返回設(shè)置的值value
return value;
}
/**
* 獲取當(dāng)前線程Thread對應(yīng)維護的ThreadLocalMap
*
* @param t the current thread 當(dāng)前線程
* @return the map 對應(yīng)維護的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- 調(diào)用set(T value)操作設(shè)置ThreadLocal中對應(yīng)當(dāng)前線程要存儲的值時勾哩,進(jìn)行了如下操作:
- 獲取當(dāng)前線程Thread對象,進(jìn)而獲取此線程對象中維護的ThreadLocalMap對象举哟。
- 判斷當(dāng)前的ThreadLocalMap是否存在:
- 如果存在思劳,則調(diào)用map.set設(shè)置此實體entry。
- 如果不存在妨猩,則調(diào)用createMap進(jìn)行ThreadLocalMap對象的初始化潜叛,并將此實體entry作為第一個值存放至ThreadLocalMap中。
示例代碼:
/**
* 設(shè)置當(dāng)前線程對應(yīng)的ThreadLocal的值
* 大多數(shù)子類都不需要重寫此方法壶硅,
* 只需要重寫 {@link #initialValue}方法代替設(shè)置當(dāng)前線程對應(yīng)的ThreadLocal的值
*
* @param value 將要保存在當(dāng)前線程對應(yīng)的ThreadLocal的值
*
*/
public void set(T value) {
// 獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 獲取此線程對象中維護的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則調(diào)用map.set設(shè)置此實體entry
map.set(this, value);
else
// 1)當(dāng)前線程Thread 不存在ThreadLocalMap對象
// 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對象的初始化
// 3)并將此實體entry作為第一個值存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 為當(dāng)前線程Thread 創(chuàng)建對應(yīng)維護的ThreadLocalMap.
*
* @param t the current thread 當(dāng)前線程
* @param firstValue 第一個要存放的ThreadLocal變量值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 調(diào)用remove()操作刪除ThreadLocal中對應(yīng)當(dāng)前線程已存儲的值時威兜,進(jìn)行了如下操作:
- 獲取當(dāng)前線程Thread對象,進(jìn)而獲取此線程對象中維護的ThreadLocalMap對象森瘪。
- 判斷當(dāng)前的ThreadLocalMap是否存在牡属, 如果存在,則調(diào)用map.remove扼睬,以當(dāng)前ThreadLocal為key刪除對應(yīng)的實體entry逮栅。
示例代碼:
/**
* 刪除當(dāng)前線程中保存的ThreadLocal對應(yīng)的實體entry
* 如果此ThreadLocal變量在當(dāng)前線程中調(diào)用 {@linkplain #get read}方法
* 則會通過調(diào)用{@link #initialValue}進(jìn)行再次初始化,
* 除非此值value是通過當(dāng)前線程內(nèi)置調(diào)用 {@linkplain #set set}設(shè)置的
* 這可能會導(dǎo)致在當(dāng)前線程中多次調(diào)用{@code initialValue}方法
*
* @since 1.5
*/
public void remove() {
// 獲取當(dāng)前線程對象中維護的ThreadLocalMap對象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在則調(diào)用map.remove
// 以當(dāng)前ThreadLocal為key刪除對應(yīng)的實體entry
m.remove(this);
}
4.問:對ThreadLocal的常用操作實際是對線程Thread中的ThreadLocalMap進(jìn)行操作窗宇,核心是ThreadLocalMap這個哈希表措伐,你能談?wù)凾hreadLocalMap的內(nèi)部底層實現(xiàn)嗎?
答:
- ThreadLocalMap的底層實現(xiàn)是一個定制的自定義HashMap哈希表,核心組成元素有:
- Entry[] table;:底層哈希表 table, 必要時需要進(jìn)行擴容军俊,底層哈希表 table.length 長度必須是2的n次方侥加。
- int size;:實際存儲鍵值對元素個數(shù) entries
- int threshold;:下一次擴容時的閾值,閾值 threshold = 底層哈希表table的長度 len * 2 / 3粪躬。當(dāng)size >= threshold時担败,遍歷table并刪除key為null的元素,如果刪除后size >= threshold*3/4時镰官,需要對table進(jìn)行擴容(詳情請查看set(ThreadLocal<?> key, Object value)方法說明)提前。
- 其中Entry[] table;哈希表存儲的核心元素是Entry,Entry包含:
- ThreadLocal<?> k泳唠;:當(dāng)前存儲的ThreadLocal實例對象
- Object value;:當(dāng)前 ThreadLocal 對應(yīng)儲存的值value
- 需要注意的是狈网,此Entry繼承了弱引用 WeakReference,所以在使用ThreadLocalMap時,發(fā)現(xiàn)key == null拓哺,則意味著此key ThreadLocal不在被引用勇垛,需要將其從ThreadLocalMap哈希表中移除。(弱引用相關(guān)問題解釋請查看 問答 5)
示例代碼:
/**
* ThreadLocalMap 是一個定制的自定義 hashMap 哈希表士鸥,只適合用于維護
* 線程對應(yīng)ThreadLocal的值. 此類的方法沒有在ThreadLocal 類外部暴露闲孤,
* 此類是私有的,允許在 Thread 類中以字段的形式聲明 础淤,
* 以助于處理存儲量大崭放,生命周期長的使用用途哨苛,
* 此類定制的哈希表實體鍵值對使用弱引用WeakReferences 作為key鸽凶,
* 但是, 一旦引用不在被使用,
* 只有當(dāng)哈希表中的空間被耗盡時建峭,對應(yīng)不再使用的鍵值對實體才會確保被 移除回收玻侥。
*/
static class ThreadLocalMap {
/**
* 實體entries在此hash map中是繼承弱引用 WeakReference,
* 使用ThreadLocal 作為 key 鍵. 請注意,當(dāng)key為null(i.e. entry.get()
* == null) 意味著此key不再被引用,此時實體entry 會從哈希表中刪除亿蒸。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 當(dāng)前 ThreadLocal 對應(yīng)儲存的值value. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始容量大小 16 -- 必須是2的n次方.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 底層哈希表 table, 必要時需要進(jìn)行擴容.
* 底層哈希表 table.length 長度必須是2的n次方.
*/
private Entry[] table;
/**
* 實際存儲鍵值對元素個數(shù) entries.
*/
private int size = 0;
/**
* 下一次擴容時的閾值
*/
private int threshold; // 默認(rèn)為 0
/**
* 設(shè)置觸發(fā)擴容時的閾值 threshold
* 閾值 threshold = 底層哈希表table的長度 len * 2 / 3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 獲取該位置i對應(yīng)的下一個位置index
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 獲取該位置i對應(yīng)的上一個位置index
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}
- ThreadLocalMap的構(gòu)造方法是延遲加載的凑兰,也就是說,只有當(dāng)線程需要存儲對應(yīng)的ThreadLocal的值時边锁,才初始化創(chuàng)建一次(僅初始化一次)姑食。初始化步驟如下:
- 初始化底層數(shù)組table的初始容量為 16。
- 獲取ThreadLocal中的threadLocalHashCode茅坛,通過threadLocalHashCode & (INITIAL_CAPACITY - 1)音半,即ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的長度 length 的方式計算該實體的存儲位置。
- 存儲當(dāng)前的實體贡蓖,key 為 : 當(dāng)前ThreadLocal value:真正要存儲的值
- 設(shè)置當(dāng)前實際存儲元素個數(shù) size 為 1
- 設(shè)置閾值setThreshold(INITIAL_CAPACITY)曹鸠,為初始化容量 16 的 2/3。
示例代碼:
/**
* 用于創(chuàng)建一個新的hash map包含 (firstKey, firstValue).
* ThreadLocalMaps 構(gòu)造方法是延遲加載的,所以我們只會在至少有一個
* 實體entry存放時斥铺,才初始化創(chuàng)建一次(僅初始化一次)彻桃。
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 table 初始容量為 16
table = new Entry[INITIAL_CAPACITY];
// 計算當(dāng)前entry的存儲位置
// 存儲位置計算等價于:
// ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的長度 length
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 存儲當(dāng)前的實體,key 為 : 當(dāng)前ThreadLocal value:真正要存儲的值
table[i] = new Entry(firstKey, firstValue);
// 設(shè)置當(dāng)前實際存儲元素個數(shù) size 為 1
size = 1;
// 設(shè)置閾值晾蜘,為初始化容量 16 的 2/3邻眷。
setThreshold(INITIAL_CAPACITY);
}
- ThreadLocal的get()操作實際是調(diào)用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,此方法快速適用于獲取某一存在key的實體 entry,否則剔交,應(yīng)該調(diào)用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法獲取肆饶,這樣做是為了最大限制地提高直接命中的性能,該方法進(jìn)行了如下操作:
- 計算要獲取的entry的存儲位置省容,存儲位置計算等價于:ThreadLocal的 hash 值 threadLocalHashCode % 哈希表的長度 length抖拴。
- 根據(jù)計算的存儲位置,獲取到對應(yīng)的實體 Entry。判斷對應(yīng)實體Entry是否存在 并且 key是否相等:
存在對應(yīng)實體Entry并且對應(yīng)key相等阿宅,即同一ThreadLocal候衍,返回對應(yīng)的實體Entry。
不存在對應(yīng)實體Entry 或者 key不相等洒放,則通過調(diào)用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法繼續(xù)查找蛉鹿。
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法操作如下:
- 獲取底層哈希表數(shù)組table,循環(huán)遍歷對應(yīng)要查找的實體Entry所關(guān)聯(lián)的位置往湿。
- 獲取當(dāng)前遍歷的entry 的 key ThreadLocal妖异,比較key是否一致,一致則返回领追。
- 如果key不一致 并且 key 為 null他膳,則證明引用已經(jīng)不存在,這是因為Entry繼承的是WeakReference绒窑,這是弱引用帶來的坑棕孙。調(diào)用expungeStaleEntry(int staleSlot)方法刪除過期的實體Entry(此方法不單獨解釋,請查看示例代碼些膨,有詳細(xì)注釋說明)蟀俊。
- key不一致 ,key也不為空订雾,則遍歷下一個位置肢预,繼續(xù)查找。
- 遍歷完畢洼哎,仍然找不到則返回null烫映。
示例代碼:
/**
* 根據(jù)key 獲取對應(yīng)的實體 entry. 此方法快速適用于獲取某一存在key的
* 實體 entry,否則谱净,應(yīng)該調(diào)用getEntryAfterMiss方法獲取窑邦,這樣做是為
* 了最大限制地提高直接命中的性能
*
* @param key 當(dāng)前thread local 對象
* @return the entry 對應(yīng)key的 實體entry, 如果不存在,則返回null
*/
private Entry getEntry(ThreadLocal<?> key) {
// 計算要獲取的entry的存儲位置
// 存儲位置計算等價于:
// ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的長度 length
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取到對應(yīng)的實體 Entry
Entry e = table[i];
// 存在對應(yīng)實體并且對應(yīng)key相等壕探,即同一ThreadLocal
if (e != null && e.get() == key)
// 返回對應(yīng)的實體Entry
return e;
else
// 不存在 或 key不一致冈钦,則通過調(diào)用getEntryAfterMiss繼續(xù)查找
return getEntryAfterMiss(key, i, e);
}
/**
* 當(dāng)根據(jù)key找不到對應(yīng)的實體entry 時,調(diào)用此方法李请。
* 直接定位到對應(yīng)的哈希表位置
*
* @param key 當(dāng)前thread local 對象
* @param i 此對象在哈希表 table中的存儲位置 index
* @param e the entry 實體對象
* @return the entry 對應(yīng)key的 實體entry, 如果不存在瞧筛,則返回null
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 循環(huán)遍歷當(dāng)前位置的所有實體entry
while (e != null) {
// 獲取當(dāng)前entry 的 key ThreadLocal
ThreadLocal<?> k = e.get();
// 比較key是否一致,一致則返回
if (k == key)
return e;
// 找到對應(yīng)的entry 导盅,但其key 為 null较幌,則證明引用已經(jīng)不存在
// 這是因為Entry繼承的是WeakReference,這是弱引用帶來的坑
if (k == null)
// 刪除過期(stale)的entry
expungeStaleEntry(i);
else
// key不一致 白翻,key也不為空乍炉,則遍歷下一個位置绢片,繼續(xù)查找
i = nextIndex(i, len);
// 獲取下一個位置的實體 entry
e = tab[i];
}
// 遍歷完畢,找不到則返回null
return null;
}
/**
* 刪除對應(yīng)位置的過期實體岛琼,并刪除此位置后對應(yīng)相關(guān)聯(lián)位置key = null的實體
*
* @param staleSlot 已知的key = null 的對應(yīng)的位置索引
* @return 對應(yīng)過期實體位置索引的下一個key = null的位置
* (所有的對應(yīng)位置都會被檢查)
*/
private int expungeStaleEntry(int staleSlot) {
// 獲取對應(yīng)的底層哈希表 table
Entry[] tab = table;
// 獲取哈希表長度
int len = tab.length;
// 擦除這個位置上的臟數(shù)據(jù)
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 直到我們找到 Entry e = null底循,才執(zhí)行rehash操作
// 就是遍歷完該位置的所有關(guān)聯(lián)位置的實體
Entry e;
int i;
// 查找該位置對應(yīng)所有關(guān)聯(lián)位置的過期實體,進(jìn)行擦除操作
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)操作實際是調(diào)用ThreadLocalMap的set(ThreadLocal<?> key, Object value)方法槐瑞,該方法進(jìn)行了如下操作:
- 獲取對應(yīng)的底層哈希表table熙涤,計算對應(yīng)threalocal的存儲位置。
- 循環(huán)遍歷table對應(yīng)該位置的實體困檩,查找對應(yīng)的threadLocal祠挫。
- 獲取當(dāng)前位置的threadLocal,如果key threadLocal一致悼沿,則證明找到對應(yīng)的threadLocal等舔,將新值賦值給找到的當(dāng)前實體Entry的value中,結(jié)束显沈。
- 如果當(dāng)前位置的key threadLocal不一致软瞎,并且key threadLocal為null逢唤,則調(diào)用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法(此方法不單獨解釋拉讯,請查看示例代碼,有詳細(xì)注釋說明)境蔼,替換該位置key == null 的實體為當(dāng)前要設(shè)置的實體扭弧,結(jié)束雹舀。
- 如果當(dāng)前位置的key threadLocal不一致,并且key threadLocal不為null院尔,則創(chuàng)建新的實體,并存放至當(dāng)前位置 i tab[i] = new Entry(key, value);喉誊,實際存儲鍵值對元素個數(shù)size + 1邀摆,由于弱引用帶來了這個問題,所以要調(diào)用cleanSomeSlots(int i, int n)方法清除無用數(shù)據(jù)(此方法不單獨解釋伍茄,請查看示例代碼栋盹,有詳細(xì)注釋說明),才能判斷現(xiàn)在的size有沒有達(dá)到閥值threshhold敷矫,如果沒有要清除的數(shù)據(jù)例获,存儲元素個數(shù)仍然 大于 閾值 則調(diào)用rehash方法進(jìn)行擴容(此方法不單獨解釋,請查看示例代碼曹仗,有詳細(xì)注釋說明)榨汤。
示例代碼:
/**
* 設(shè)置對應(yīng)ThreadLocal的值
*
* @param key 當(dāng)前thread local 對象
* @param value 要設(shè)置的值
*/
private void set(ThreadLocal<?> key, Object value) {
// 我們不會像get()方法那樣使用快速設(shè)置的方式,
// 因為通常很少使用set()方法去創(chuàng)建新的實體
// 相對于替換一個已經(jīng)存在的實體, 在這種情況下,
// 快速設(shè)置方案會經(jīng)常失敗怎茫。
// 獲取對應(yīng)的底層哈希表 table
Entry[] tab = table;
// 獲取哈希表長度
int len = tab.length;
// 計算對應(yīng)threalocal的存儲位置
int i = key.threadLocalHashCode & (len-1);
// 循環(huán)遍歷table對應(yīng)該位置的實體收壕,查找對應(yīng)的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 獲取當(dāng)前位置的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果key threadLocal一致,則證明找到對應(yīng)的threadLocal
if (k == key) {
// 賦予新值
e.value = value;
// 結(jié)束
return;
}
// 如果當(dāng)前位置的key threadLocal為null
if (k == null) {
// 替換該位置key == null 的實體為當(dāng)前要設(shè)置的實體
replaceStaleEntry(key, value, i);
// 結(jié)束
return;
}
}
// 當(dāng)前位置的k != key && k != null
// 創(chuàng)建新的實體蜜宪,并存放至當(dāng)前位置i
tab[i] = new Entry(key, value);
// 實際存儲鍵值對元素個數(shù) + 1
int sz = ++size;
// 由于弱引用帶來了這個問題旬渠,所以先要清除無用數(shù)據(jù),才能判斷現(xiàn)在的size有沒有達(dá)到閥值threshhold
// 如果沒有要清除的數(shù)據(jù)端壳,存儲元素個數(shù)仍然 大于 閾值 則擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 擴容
rehash();
}
/**
* 當(dāng)執(zhí)行set操作時告丢,獲取對應(yīng)的key threadLocal,并替換過期的實體
* 將這個value值存儲在對應(yīng)key threadLocal的實體中损谦,無論是否已經(jīng)存在體
* 對應(yīng)的key threadLocal
*
* 有一個副作用, 此方法會刪除該位置下和該位置nextIndex對應(yīng)的所有過期的實體
*
* @param key 當(dāng)前thread local 對象
* @param value 當(dāng)前thread local 對象對應(yīng)存儲的值
* @param staleSlot 第一次找到此過期的實體對應(yīng)的位置索引index
* .
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
// 獲取對應(yīng)的底層哈希表 table
Entry[] tab = table;
// 獲取哈希表長度
int len = tab.length;
Entry e;
// 往前找岖免,找到table中第一個過期的實體的下標(biāo)
// 清理整個table是為了避免因為垃圾回收帶來的連續(xù)增長哈希的危險
// 也就是說,哈希表沒有清理干凈照捡,當(dāng)GC到來的時候颅湘,后果很嚴(yán)重
// 記錄要清除的位置的起始首位置
int slotToExpunge = staleSlot;
// 從該位置開始,往前遍歷查找第一個過期的實體的下標(biāo)
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栗精,那么我們就需要把它跟新的過期數(shù)據(jù)交換來保持哈希表的順序
// 那么剩下的過期Entry呢闯参,就可以交給expungeStaleEntry方法來擦除掉
// 將新設(shè)置的實體放置在此過期的實體的位置上
if (k == key) {
// 替換,將要設(shè)置的值放在此過期的實體中
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果存在悲立,則開始清除之前過期的實體
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 在這里開始清除過期數(shù)據(jù)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// / 如果我們沒有在往后查找中找沒有找到過期的實體鹿寨,
// 那么slotToExpunge就是第一個過期Entry的下標(biāo)了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 最后key仍沒有找到,則將要設(shè)置的新實體放置
// 在原過期的實體對應(yīng)的位置上薪夕。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果該位置對應(yīng)的其他關(guān)聯(lián)位置存在過期實體脚草,則清除
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 啟發(fā)式的掃描查找一些過期的實體并清除,
* 此方法會再添加新實體的時候被調(diào)用,
* 或者過期的元素被清除時也會被調(diào)用.
* 如果實在沒有過期數(shù)據(jù)原献,那么這個算法的時間復(fù)雜度就是O(log n)
* 如果有過期數(shù)據(jù)馏慨,那么這個算法的時間復(fù)雜度就是O(n)
*
* @param i 一個確定不是過期的實體的位置,從這個位置i開始掃描
*
* @param n 掃描控制: 有{@code log2(n)} 單元會被掃描,
* 除非找到了過期的實體, 在這種情況下
* 有{@code log2(table.length)-1} 的格外單元會被掃描.
* 當(dāng)調(diào)用插入時, 這個參數(shù)的值是存儲實體的個數(shù)姑隅,
* 但如果調(diào)用 replaceStaleEntry方法, 這個值是哈希表table的長度
* (注意: 所有的這些都可能或多或少的影響n的權(quán)重
* 但是這個版本簡單写隶,快速,而且似乎執(zhí)行效率還可以)
*
* @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加倍以避免滯后
// 存儲實體個數(shù) 大于等于 閾值的3/4則擴容
if (size >= threshold - threshold / 4)
resize();
}
/**
* 擴容方法秩贰,以2倍的大小進(jìn)行擴容
* 擴容的思想跟HashMap很相似,都是把容量擴大兩倍
* 不同之處還是因為WeakReference帶來的
*/
private void resize() {
// 記錄舊的哈希表
Entry[] oldTab = table;
// 記錄舊的哈希表長度
int oldLen = oldTab.length;
// 新的哈希表長度為舊的哈希表長度的2倍
int newLen = oldLen * 2;
// 創(chuàng)建新的哈希表
Entry[] newTab = new Entry[newLen];
int count = 0;
// 逐一遍歷舊的哈希表table的每個實體柔吼,重新分配至新的哈希表中
for (int j = 0; j < oldLen; ++j) {
// 獲取對應(yīng)位置的實體
Entry e = oldTab[j];
// 如果實體不會null
if (e != null) {
// 獲取實體對應(yīng)的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果該ThreadLocal 為 null
if (k == null) {
// 則對應(yīng)的值也要清除
// 就算是擴容毒费,也不能忘了為擦除過期數(shù)據(jù)做準(zhǔn)備
e.value = null; // Help the GC
} else {
// 如果不是過期實體,則根據(jù)新的長度重新計算存儲位置
int h = k.threadLocalHashCode & (newLen - 1);
// 將該實體存儲在對應(yīng)ThreadLocal的最后一個位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 重新分配位置完畢愈魏,則重新計算閾值Threshold
setThreshold(newLen);
// 記錄實際存儲元素個數(shù)
size = count;
// 將新的哈希表賦值至底層table
table = newTab;
}
ThreadLocal的remove()操作實際是調(diào)用ThreadLocalMap的remove(ThreadLocal<?> key)方法觅玻,該方法進(jìn)行了如下操作:
1 ) 獲取對應(yīng)的底層哈希表 table想际,計算對應(yīng)threalocal的存儲位置。
2 ) 循環(huán)遍歷table對應(yīng)該位置的實體溪厘,查找對應(yīng)的threadLocal胡本。
3 ) 獲取當(dāng)前位置的threadLocal,如果key threadLocal一致畸悬,則證明找到對應(yīng)的threadLocal侧甫,執(zhí)行刪除操作,刪除此位置的實體蹋宦,結(jié)束披粟。
示例代碼:
/**
* 移除對應(yīng)ThreadLocal的實體
*/
private void remove(ThreadLocal<?> key) {
// 獲取對應(yīng)的底層哈希表 table
Entry[] tab = table;
// 獲取哈希表長度
int len = tab.length;
// 計算對應(yīng)threalocal的存儲位置
int i = key.threadLocalHashCode & (len-1);
// 循環(huán)遍歷table對應(yīng)該位置的實體,查找對應(yīng)的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 如果key threadLocal一致冷冗,則證明找到對應(yīng)的threadLocal
if (e.get() == key) {
// 執(zhí)行清除操作
e.clear();
// 清除此位置的實體
expungeStaleEntry(i);
// 結(jié)束
return;
}
}
}
- 問:ThreadLocalMap中的存儲實體Entry使用ThreadLocal作為key守屉,但這個Entry是繼承弱引用WeakReference的,為什么要這樣設(shè)計蒿辙,使用了弱引用WeakReference會造成內(nèi)存泄露問題嗎拇泛?
答:首先,回答這個問題之前思灌,我需要解釋一下什么是強引用俺叭,什么是弱引用。
我們在正常情況下习瑰,普遍使用的是強引用:
A a = new A();
B b = new B();當(dāng) a = null;b = null;時绪颖,一段時間后,JAVA垃圾回收機制GC會將 a 和 b 對應(yīng)所分配的內(nèi)存空間給回收甜奄。
但考慮這樣一種情況:
C c = new C(b);
b = null;
當(dāng) b 被設(shè)置成null時,那么是否意味這一段時間后GC工作可以回收 b 所分配的內(nèi)存空間呢窃款?答案是否定的课兄,因為即使 b 被設(shè)置成null,但 c 仍然持有對 b 的引用晨继,而且還是強引用烟阐,所以GC不會回收 b 原先所分配的空間,既不能回收紊扬,又不能使用蜒茄,這就造成了 內(nèi)存泄露。
那么如何處理呢餐屎?
可以通過c = null;檀葛,也可以使用弱引用WeakReference w = new WeakReference(b);。因為使用了弱引用WeakReference腹缩,GC是可以回收 b 原先所分配的空間的屿聋。
回到ThreadLocal的層面上空扎,ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它润讥,那么系統(tǒng) GC 的時候转锈,這個ThreadLocal勢必會被回收,這樣一來楚殿,ThreadLocalMap中就會出現(xiàn)key為null的Entry撮慨,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話脆粥,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠(yuǎn)無法回收甫煞,造成內(nèi)存泄漏。
其實冠绢,ThreadLocalMap的設(shè)計中已經(jīng)考慮到這種情況抚吠,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。
但是這些被動的預(yù)防措施并不能保證不會內(nèi)存泄漏:
- 使用static的ThreadLocal弟胀,延長了ThreadLocal的生命周期楷力,可能導(dǎo)致的內(nèi)存泄漏。
- 分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法孵户,那么就會導(dǎo)致內(nèi)存泄漏萧朝。
從表面上看內(nèi)存泄漏的根源在于使用了弱引用。網(wǎng)上的文章大多著重分析ThreadLocal使用了弱引用會導(dǎo)致內(nèi)存泄漏夏哭,但是另一個問題也同樣值得思考:為什么使用弱引用而不是強引用检柬?
我們先來看看官方文檔的說法:
To help deal with very large and long-lived usages,
the hash table entries use WeakReferences for keys.
為了應(yīng)對非常大和長時間的用途,哈希表使用弱引用的 key竖配。
下面我們分兩種情況討論:
key 使用強引用:引用的ThreadLocal的對象被回收了何址,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除进胯,ThreadLocal不會被回收用爪,導(dǎo)致Entry內(nèi)存泄漏。
key 使用弱引用:引用的ThreadLocal的對象被回收了胁镐,由于ThreadLocalMap持有ThreadLocal的弱引用偎血,即使沒有手動刪除,ThreadLocal也會被回收盯漂。value在下一次ThreadLocalMap調(diào)用get(),set(),remove()的時候會被清除颇玷。
比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長就缆,如果都沒有手動刪除對應(yīng)key帖渠,都會導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內(nèi)存泄漏违崇,對應(yīng)的value在下一次ThreadLocalMap調(diào)用get(),set(),remove()的時候會被清除阿弃。
因此诊霹,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應(yīng)key就會導(dǎo)致內(nèi)存泄漏渣淳,而不是因為弱引用脾还。
綜合上面的分析,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果入愧,那么怎么避免內(nèi)存泄漏呢鄙漏?
每次使用完ThreadLocal,都調(diào)用它的remove()方法棺蛛,清除數(shù)據(jù)怔蚌。
在使用線程池的情況下,沒有及時清理ThreadLocal旁赊,不僅是內(nèi)存泄漏的問題桦踊,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題。所以终畅,使用ThreadLocal就跟加鎖完要解鎖一樣籍胯,用完就清理。
- 問:ThreadLocal和synchronized的區(qū)別?
答:ThreadLocal和synchronized關(guān)鍵字都用于處理多線程并發(fā)訪問變量的問題离福,只是二者處理問題的角度和思路不同杖狼。
ThreadLocal是一個Java類,通過對當(dāng)前線程中的局部變量的操作來解決不同線程的變量訪問的沖突問題。所以妖爷,ThreadLocal提供了線程安全的共享對象機制蝶涩,每個線程都擁有其副本。
Java中的synchronized是一個保留字絮识,它依靠JVM的鎖機制來實現(xiàn)臨界區(qū)的函數(shù)或者變量的訪問中的原子性绿聘。在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量笋除。此時斜友,被用作“鎖機制”的變量時多個線程共享的。
同步機制(synchronized關(guān)鍵字)采用了以“時間換空間”的方式垃它,提供一份變量,讓不同的線程排隊訪問烹看。而ThreadLocal采用了“以空間換時間”的方式国拇,為每一個線程都提供一份變量的副本,從而實現(xiàn)同時訪問而互不影響惯殊。
- 問:ThreadLocal在現(xiàn)時有什么應(yīng)用場景酱吝?
答:總的來說ThreadLocal主要是解決2種類型的問題:
解決并發(fā)問題:使用ThreadLocal代替synchronized來保證線程安全。同步機制采用了“以時間換空間”的方式土思,而ThreadLocal采用了“以空間換時間”的方式务热。前者僅提供一份變量忆嗜,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量崎岂,因此可以同時訪問而互不影響捆毫。
解決數(shù)據(jù)存儲問題:ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,所以每個線程可以訪問自己內(nèi)部的副本變量冲甘,不同線程之間不會互相干擾绩卤。如一個Parameter對象的數(shù)據(jù)需要在多個模塊中使用,如果采用參數(shù)傳遞的方式江醇,顯然會增加模塊之間的耦合性濒憋。此時我們可以使用ThreadLocal解決。
應(yīng)用場景:
Spring使用ThreadLocal解決線程安全問題
我們知道在一般情況下陶夜,只有無狀態(tài)的Bean才可以在多線程環(huán)境下共享凛驮,在Spring中,絕大部分Bean都可以聲明為singleton作用域条辟。就是因為Spring對一些Bean(如RequestContextHolder黔夭、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態(tài)采用ThreadLocal進(jìn)行處理捂贿,讓它們也成為線程安全的狀態(tài)纠修,因為有狀態(tài)的Bean就可以在多線程中共享了。
一般的Web應(yīng)用劃分為展現(xiàn)層厂僧、服務(wù)層和持久層三個層次扣草,在不同的層中編寫對應(yīng)的邏輯,下層通過接口向上層開放功能調(diào)用颜屠。在一般情況下辰妙,從接收請求到返回響應(yīng)所經(jīng)過的所有程序調(diào)用都同屬于一個線程ThreadLocal是解決線程安全問題一個很好的思路,它通過為每個線程提供一個獨立的變量副本解決了變量并發(fā)訪問的沖突問題甫窟。在很多情況下密浑,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便粗井,且結(jié)果程序擁有更高的并發(fā)性尔破。
示例代碼:
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");
}
總結(jié)
ThreadLocal提供線程內(nèi)部的局部變量,在本線程內(nèi)隨時隨地可取浇衬,隔離其他線程懒构。
ThreadLocal的設(shè)計是:每個Thread維護一個ThreadLocalMap哈希表,這個哈希表的key是ThreadLocal實例本身耘擂,value才是真正要存儲的值Object胆剧。
對ThreadLocal的常用操作實際是對線程Thread中的ThreadLocalMap進(jìn)行操作。
ThreadLocalMap的底層實現(xiàn)是一個定制的自定義HashMap哈希表醉冤,ThreadLocalMap的閾值threshold = 底層哈希表table的長度 len * 2 / 3秩霍,當(dāng)實際存儲元素個數(shù)size 大于或等于 閾值threshold的 3/4 時size >= threshold*3/4篙悯,則對底層哈希表數(shù)組table進(jìn)行擴容操作。
ThreadLocalMap中的哈希表Entry[] table存儲的核心元素是Entry铃绒,存儲的key是ThreadLocal實例對象鸽照,value是ThreadLocal 對應(yīng)儲存的值value。需要注意的是匿垄,此Entry繼承了弱引用 WeakReference移宅,所以在使用ThreadLocalMap時,發(fā)現(xiàn)key == null椿疗,則意味著此key ThreadLocal不在被引用漏峰,需要將其從ThreadLocalMap哈希表中移除。
ThreadLocalMap使用ThreadLocal的弱引用作為key届榄,如果一個ThreadLocal沒有外部強引用來引用它浅乔,那么系統(tǒng) GC 的時候,這個ThreadLocal勢必會被回收铝条。所以靖苇,在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。如果我們不主動調(diào)用上述操作班缰,則會導(dǎo)致內(nèi)存泄露贤壁。
為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣埠忘,在每次使用完ThreadLocal后都要調(diào)用remove()來清理無用的Entry脾拆。這在操作在使用線程池時尤為重要。
ThreadLocal和synchronized的區(qū)別:同步機制(synchronized關(guān)鍵字)采用了以“時間換空間”的方式莹妒,提供一份變量名船,讓不同的線程排隊訪問。而ThreadLocal采用了“以空間換時間”的方式旨怠,為每一個線程都提供一份變量的副本渠驼,從而實現(xiàn)同時訪問而互不影響。
ThreadLocal主要是解決2種類型的問題:A. 解決并發(fā)問題:使用ThreadLocal代替同步機制解決并發(fā)問題鉴腻。B. 解決數(shù)據(jù)存儲問題:如一個Parameter對象的數(shù)據(jù)需要在多個模塊中使用迷扇,如果采用參數(shù)傳遞的方式,顯然會增加模塊之間的耦合性爽哎。此時我們可以使用ThreadLocal解決谋梭。
歡迎關(guān)注我的公眾號