ThreadLocal本地線程變量原理解析

在Java多線程併發環境下,如果我們需要對某一個變量進行操作的話,很有可能將造成線程安全問題,爲了解決這種線程安全問題,我們可以給操作這個變量的方法或代碼塊加各種鎖,雖然可以實現線程安全,但這樣做會使系統性能受一定的影響,又比如在某個業務場景下,需要多個線程來同時操作每個用戶或每筆訂單的信息,爲了保證線程安全,需要將這些變量都作爲每個線程獨有的變量,這種情況下,我們可以考慮使用Java中提供的ThreadLocal類來實現,它可以實現每個線程都有屬於自己的本地變量副本,從而實現變量的線程安全及其他業務需求。

在瞭解ThreadLocal的原理前,先貼一個例子來驗證ThreadLocal是否實現了每個線程都擁有自己的變量副本

public class ThreadLocalMain {
    private static ThreadLocal<String> local = new ThreadLocal<>();
    public static void main(String[] args) {
        new Thread(() -> {
            local.set("馬超");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ",local -> "+local.get());

        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            local.set("曹操");
            System.out.println(Thread.currentThread().getName() + ",local -> "+local.get());
        }).start();
    }
}

根據上面的運行結果,兩個線程操作同一個ThreadLocal中的值,確實沒有出現覆蓋的情況。

那麼它是如何實現的呢?

下面通過ThreadLocal的源碼來了解它的實現原理。

這裏我們主要了解它的兩個核心方法

set(T value);
get();

先從set方法開始

/**
* Sets the current thread's copy of this thread-local variable
* to the specified value.  Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
*        this thread-local.
*/
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

從方法上方的註釋及代碼來看,可以知道這個方法可以將一個傳入的值set到當前線程的本地副本中。

在方法第一行代碼中,獲取到當前的線程t,再通過getMap方法傳入當前線程,來獲取這個線程中的threadLocals變量,這個變量是ThreadLocal中的一個內部類ThreadLocalMap,用來保存變量副本。接着判斷這個獲取到的這個map是否爲空(有沒有被實例化,因爲這個類是延遲構造的),如果不爲空則調用ThreadLocalMap的set方法傳入當前的引用this及value來設置其中的值,否則調用createMap方法來爲當前線程實例化一個ThreadLocalMap,ThreadLocalMap類的內部維護一個Entry[] table數組變量,用來保存本地線程變量的副本,它的結構類似於一個map集合,鍵爲當前操作的這個ThreadLocal(也就是傳入的this),值爲set方法傳入的value。

我們可以點進來看一下這個createMap方法是如何實例化一個ThreadLocalMap的。

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}

通過這個ThreadLocalMap的構造函數,可以看到傳入的ThreadLocal作爲key,和傳入的value通過hash計算來找到table中的某個位置進行存放。

看完set方法的原理後,瞭解了ThreadLocal是如何存儲每個線程的獨立變量副本了。

最後再來看一下get方法,和set方法同理,先貼一下源碼

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

既然set方法是將值保存在Thread中的ThreadLocalMap,那麼get方法也就是通過當前的ThreadLocal作爲key,從ThreadLocalMap中的table變量表中取出對應的變量。在代碼的最後一行中,setInitialValue()方法用於當你將ThreadLocal作爲key取不到數據時(ThreadLocalMap爲空,也就是沒有調用set方法,直接調用get),會將一個null值保存到table變量表中,此時並不會報空指針異常,而是拿到一個null值。

另外ThreadLocalMap在使用的時候也會造成一些新的問題,例如如果我們的線程一般都是交給線程池來管理的,而線程池的核心就是重複使用這些已經創建好的線程來執行任務,所以當我們的一個線程執行完畢後,後面進來的任務執行時如果分配到這個線程,在未調用set方法情況下就很有可能會get到這個線程中ThreadLocalMap保存的變量,造成讀到的數據爲髒數據。

除了這個問題外,還可能有引起內存泄漏的問題,原因在於在ThreadLocalMap中保存的變量key使用了指向ThreadLocal的弱引用(WeakReference),當ThreadLocal沒有任何強引用關係後,這個在ThreadLocalMap中引用這個ThreadLocal的key也會被回收,變成null,而它對應的value值是不會被回收的,那麼將造成內存泄漏問題,除非在當前該線程執行完畢出棧後,也就不存在這個value無法被回收的情況,但是如果我們使用線程池,線程即使執行完任務也不會被銷燬,而是在線程池中保持活躍,等待新的任務,這種情況下,將可能引起內存泄漏問題。

爲了解決以上出現的問題,我們在使用完這個變量後,最好調用一下remove方法將其刪除。


最後,如果你覺得寫的還可以的話,請在右上角(app左下角)點個贊吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章