前言
ThreadLocal想必都不陌生,當多線程訪問同一個共享變量時,就容易出現並發問題,為了保證線程安全,我們需要對共享變量進行同步加鎖,但這又帶來了性能消耗以及使用者的負擔,那麼有沒有可能當我們創建一個共享變量時,每個線程對其訪問的時候訪問的都是自己線程的變量呢?沒錯那就是ThreadLocal。
ThreadLocal使用
舉個簡單例子: 比如實現一些數據運算的操作,過程中可能需要藉助一個臨時表去處理數據,臨時表有一列存的每一次的執行ID,執行完成根據此次的執行ID進行刪除臨時表數據。可以使用一個ThreadLocal來存儲當前線程的執行ID。(此處暫不考慮性能問題)
@Service
public class DataSyncServiceImpl {
private final Logger logger = LoggerFactory.getLogger(DataSyncServiceImpl.class);
private static final ThreadLocal<String> execLocalId = ThreadLocal.withInitial(()->new String());
@Autowired
private jdbcTemplate jdbcTemplate;
/**
* 藉助臨時表進行數據運算操作
* 臨時表欄位(id,execution_id)
*/
public void calculateData(String key){
try {
execLocalId.set(UUID.randomUUID().toString());
calculate();
check();
System.out.println("同步數據...");
}finally {
destory();
}
}
private void calculate(){
try {
System.out.println("數據運算");
String execId = execLocalId.get();
//...
Thread.sleep(1000L);
} catch (InterruptedException e) {
logger.error("執行異常!",e);
}
}
private void check(){
try {
System.out.println("數據運算");
String execId = execLocalId.get();
//...
Thread.sleep(1000L);
} catch (InterruptedException e) {
logger.error("執行異常!",e);
}
}
private void destory(){
//根據execution_id刪除臨時表數據
StringBuffer sql = new StringBuffer();
sql.append("delete from temp_table where execution_id = ?");
jdbcTemplate.update(sql.toString(),execLocalId.get());
execLocalId.remove();
}
}
這樣的話保證了每一個請求線程都有自己的執行ID,清除數據時互不影響。
ThreadLocal實現原理
進入Thread類,可以看到這樣兩個變量,threadLocals和inheritableThreadLocals,他們都是ThreadLocalMap類型,而ThreadLocalmap是一個類似Map的結構。默認情況下兩個變量都為null,當前線程調用set或者get時才會創建。也就是說ThreadLocal變量其實是存在調用線程的內存空間中。每個Thread線程都保存了一個共享變量的副本。
- threadLocals:當前線程的ThreadLocal變量
- inheritableThreadLocals:解決子線程不能訪問父線程中的ThreadLocal變量
ThreadLocalMap
ThreadLocalMap是一個key為ThreadLocal本身,值為存入的value,對於不同的線程,每次獲取副本時,別的線程不能獲取到當前線程的副本值,形成了隔離。
Thread和ThreadLocal的關係
Set方法源碼分析
/**
* 返回當前線程中保存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對象
第二種情況: map存在, 但是沒有與當前ThreadLocal關聯的entry
*/
return setInitialValue();
}
/**
* 初始化
* @return the initial value 初始化後的值
*/
private T setInitialValue() {
// 調用initialValue獲取初始化的值
// 此方法可以被子類重寫, 如果不重寫默認返回null
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)並將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
createMap(t, value);
// 返回設置的值value
return value;
}
執行步驟:
- 獲取當前線程,根據當前線程獲取到ThreadlocalMap,即threadLocals;
- 如果獲取到的Map不為空,則設置value,key為調用此方法的ThreadLocal引用;
- 如果Map為空,則先調用createMap創建,再設置value。
Set方法源碼分析
/**
* 返回當前線程中保存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對象
第二種情況: map存在, 但是沒有與當前ThreadLocal關聯的entry
*/
return setInitialValue();
}
/**
* 初始化
* @return the initial value 初始化後的值
*/
private T setInitialValue() {
// 調用initialValue獲取初始化的值
// 此方法可以被子類重寫, 如果不重寫默認返回null
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)並將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
createMap(t, value);
// 返回設置的值value
return value;
}
執行步驟:
- 獲取當前線程,獲取此線程對象中維護的ThreadLocalMap對象;
- 如果Map不為空,則通過當前調用的ThreadLocal對象獲取Entry;
- 判斷Entry不為空,則直接返回value;
- Map或Entry為空,則通過initialValue函數獲取初始值value,然後用ThreadLocal的引用和value作為Key和Value創建一個新的Map。
內存泄漏
從ThreadLocal整體設計上我們可以看到,key持有ThreadLocal的弱引用,GC的時候會被回收,即Entry的key為null。但是當我們沒有手動刪除這個Entry或者線程一直運行的前提下,存在有強引用鏈 threadRef->currentThread->threadLocalMap->entry -> value ,value不會被回收,導致內存泄漏。
出現內存泄漏的情況:
- 沒有手動刪除對應的Entry節點信息,value一直存在。
- ThreadLocal 對象使用完後,對應線程仍然在運行。
避免內存泄露:
- 使用完ThreadLocal,調用其remove方法刪除對應的Entry。
- 對於第二種情況,因為使用了弱引用,當ThreadLocal 使用完後,key的引用就會為null,而在調用ThreadLocal 中的get()/set()方法時,當判斷key為null時會將value置為null,這就就會在jvm下次GC時將對應的Entry對象回收,從而避免內存泄漏問題的出現。
總結
本文主要講解了ThreadLocal的作用及基本用法,以及ThreadLocal的實現原理和基礎方法,注意事項。最後,用ThreadLocal一定要記得用完remove!