天天看點

簡述 ThreadLocal 原理

簡述ThreadLocal 使用和原理

線上程的調用中可以通過ThreadLocal進行的參數傳輸,減少方法中的參數一層層的嵌套下去,而且它是線程安全的,通過使用ThreadLocal 的方式可以在代碼和多線程的情況下處理很多線程安全的問題。

那它是怎樣做到線上程裡面裡面做到線程安全的呢?

那它是通過怎樣的格式儲存資料的呢?

存在多資料的時候是否會導緻記憶體洩露呢?

接下來将探讨下ThreadLocal底層的實作

簡單 main 方法示例:

public class TreadLocalDemo {


    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            threadLocal1.set("11111");
            threadLocal2.set("22222");
            System.out.println(threadLocal1.get());
            System.out.println(threadLocal2.get());
            threadLocal2.remove();
        }).start();
    }
}
           

得到結果:

簡述 ThreadLocal 原理

這裡可以看到 通過set方法就可以 在同一個線程下面每個地方可以通過擷取到threadLocal 的引用就可以通過Get 方法擷取到set進去的值.

上面隻是簡單示範下,你學會後可以有各種各樣的變體。

這裡是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);
    }
           

這裡可以看到通過擷取到目前Thread 然後擷取之前是否已經存在了ThreadLocalMap 對象;

如果目前線程沒有ThreadLocalMap 就會建立一個,并且把引用設定會Thread 對象裡面,建立ThreadLocalMap 時初始化一個預設值為16的 Entry[]  ,并且Entry對應的key就是目前ThreadLocal的引用,而且是個弱應用(後面會描述他們的關系),value就是set的值。

void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        	// 建立一個預設長度等于16的數組
            table = new Entry[INITIAL_CAPACITY];
            // 通過目前ThreadLocal hashcode值 和 目前的容量 取 & 
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        	// 設定值
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 是這裡計算下一次擴充臨界值長度
            setThreshold(INITIAL_CAPACITY);
        }
           

關于 firstKey.threadLocalHashCode 計算方式可以參考: https://zhuanlan.zhihu.com/p/40515974  

如果目前對象已經存在ThreadLocalMap的話 

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
           

這裡代碼的大體已經就是通過計算出目前threadLocal的索引值 然後把值設定到對應的ThreadLocalMap的Entry對應的數組中。

這裡描述ThreadLocal 、ThreadLocalMap、Entry[]、Entry 它們的關系:

一個Thread,裡面隻有一個THreadLocalMap,在ThreadLocalMap 裡面是以會有一個Entry[],而且在建立Entry對象的時候 Key是threadLcoal、value 是設定的值

簡述 ThreadLocal 原理

上圖可以說明:

如果我們目前Thread 執行完成後就會斷開強引用,隻有一個弱引用,這樣弱引用對象來說就會被垃圾回收機制給回收,防止記憶體溢出。

難道這樣就不會産生記憶體溢出嗎?假如設定 threadLocal = null ,而且目前Thread還存活這時候。Entry對象裡面的Key是弱應用他會不會被回收 ? 不會(因為它本身其實是線程本身強引用,隻有當不存線上程的強引用之後,這個weakreference才會在GC的時候被端),但是這時候Entry 對象裡面的 key = null 這樣下來不一樣會存在不斷的記憶體占用嗎? 一樣有可能導緻記憶體的溢出。

當然上面說的設計者也想到了,是以分别在執行set、get、remove的方式裡面都會嘗試或者直接去檢查key = null 的值去移除。(這裡并不是所有的set、get操作都會去執行,remove 是先設定成null然後去移除 可以去看看源碼裡面的這個方法 expungeStaleEntry )

也許你留意到我上面的main方法的代碼裡面的ThreadLocal是一個靜态的對象,如果你把他加上一個final 的話 ,上面是不是就成為一個僞命題了。。。具體實作Thread 寫法很多。但是建議最後還是我們通過手動remove() 方法進行移除。 

可能有人會問為什麼需要把存儲對應格式設計成數組,那你可以看看我一開始的main方法,因為一個Thread裡面可以持有一個或者多個以上的ThreadLcoal的引用,這樣的話就需要數組來進行存儲了。

舉例使用場景:

1、spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,隻有無狀态的Bean才可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如 RequestContextHolder、 TransactionSynchronizationManager、 LocaleContextHolder等)中非線程安全狀态采用ThreadLocal進行處理讓它們也成為線程安全的狀态。 2、PageHelper找那個startPage就是通過ThreadLocal來進行儲存目前分頁資訊。

這裡隻是本人對ThreadLocal一些簡單的見解,如果有錯誤之處請諒解,望指出,謝謝!!!