至今還沒搞懂ThreadLocal怎麼玩?

是啊超ya 發佈 2024-04-28T16:34:06.555693+00:00

ThreadLocal想必都不陌生,當多線程訪問同一個共享變量時,就容易出現並發問題,為了保證線程安全,我們需要對共享變量進行同步加鎖,但這又帶來了性能消耗以及使用者的負擔,那麼有沒有可能當我們創建一個共享變量時,每個線程對其訪問的時候訪問的都是自己線程的變量呢?

前言

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;
    }


執行步驟:

  1. 獲取當前線程,根據當前線程獲取到ThreadlocalMap,即threadLocals;
  2. 如果獲取到的Map不為空,則設置value,key為調用此方法的ThreadLocal引用;
  3. 如果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;
    }


執行步驟:

  1. 獲取當前線程,獲取此線程對象中維護的ThreadLocalMap對象;
  2. 如果Map不為空,則通過當前調用的ThreadLocal對象獲取Entry;
  3. 判斷Entry不為空,則直接返回value;
  4. Map或Entry為空,則通過initialValue函數獲取初始值value,然後用ThreadLocal的引用和value作為Key和Value創建一個新的Map。

內存泄漏

從ThreadLocal整體設計上我們可以看到,key持有ThreadLocal的弱引用,GC的時候會被回收,即Entry的key為null。但是當我們沒有手動刪除這個Entry或者線程一直運行的前提下,存在有強引用鏈 threadRef->currentThread->threadLocalMap->entry -> value ,value不會被回收,導致內存泄漏。

出現內存泄漏的情況:

  1. 沒有手動刪除對應的Entry節點信息,value一直存在。
  2. ThreadLocal 對象使用完後,對應線程仍然在運行。

避免內存泄露:

  1. 使用完ThreadLocal,調用其remove方法刪除對應的Entry。
  2. 對於第二種情況,因為使用了弱引用,當ThreadLocal 使用完後,key的引用就會為null,而在調用ThreadLocal 中的get()/set()方法時,當判斷key為null時會將value置為null,這就就會在jvm下次GC時將對應的Entry對象回收,從而避免內存泄漏問題的出現。

總結

本文主要講解了ThreadLocal的作用及基本用法,以及ThreadLocal的實現原理和基礎方法,注意事項。最後,用ThreadLocal一定要記得用完remove!

關鍵字: