一文帶你全面認識ThreadLocal源碼最詳細解讀(內容附帶真實案例加源碼分析)

目錄:

  1. 什麼是ThreadLocal
  2. ThreadLocal使用示例
  3. 真實案例剖析
  4. 深入ThreadLocal源碼
  5. ThreadLocal爲什麼會內存泄漏
  6. 總結

什麼是ThreadLocal?

ThreadLocal是Java裏一種特殊的變量。有些夥伴喜歡把它和線程同步機制混爲一談,事實上ThreadLocal與線程同步無關。ThreadLocal雖然提供了一種解決多線程環境下成員變量的問題,但是它並不是解決多線程共享變量的問題。

ThreadLocal與線程同步機制不同,線程同步機制是多個線程共享同一個變量,而ThreadLocal是爲每一個線程創建一個單獨的變量副本,故而每個線程都可以獨立地改變自己所擁有的變量副本,而不會影響其他線程所對應的副本。可以說ThreadLocal爲多線程環境下變量問題提供了另外一種解決思路。

注意: ThreadLocal實例本身是不存儲值,它只是提供了一個在當前線程中找到副本值得key

ThreadLocal源碼中有哪些方法?

在這裏插入圖片描述 在這些方法中其實我們主要關注這四個方法:

  1. get():返回此線程局部變量的當前線程副本中的值。
  2. initialValue():返回此線程局部變量的當前線程的“初始值”。
  3. remove():移除此線程局部變量當前線程的值。
  4. set(T value):將此線程局變量的當前線程副本中的值設置爲指定值。

除了這四個方法,ThreadLocal內部還有一個靜態內部類

  1. ThreadLocalMap:該內部類纔是實現線程隔離機制的關鍵,get()、set()、remove()都是基於該內部類操作。

ThreadLocalMap提供了一種用鍵值對方式存儲每一個線程的變量副本的方法,key爲當前ThreadLocal對象,value則是對應線程的變量副本。

Thread、ThreadLocal、ThreadLocalMap的關係如下;

Thread
	--ThreadLocal(屬於Thread的成員變量,而不是Thread包含在ThreadLocal中,不要搞錯了哦)
	   --ThreadLocalMap(屬於ThreadLocal的靜態內部類)

ThreadLocal使用示例

public class ThreadLocalTest {
    private static ThreadLocal<Integer> total  = new ThreadLocal<Integer>(){
        // 實現initialValue()
        @Override
        public Integer initialValue() {
            return 0;
        }
    };

    public static int addOne(){
        total.set(total.get()+1);
        return total.get();
    }

    public static void main(String[] args) {
        Runnable task = () -> {
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"  :ThreadLocalTest"+ThreadLocalTest.addOne());
            }
        };
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

運行結果: 在這裏插入圖片描述 從運行結果可以看出,ThreadLocal確實是可以達到線程隔離機制,確保變量的安全性。

看了這個例子可能很多小夥伴還不知道在實際場景是怎麼用的,對吧?那我這裏再講一個實際工作開發中的遇到的一個真實案例;

真實案例剖析:

一天,張大胖上午遇到了一個棘手的問題,他在一個AccountService中寫了一段類似這樣的代碼:

Context ctx = new Context();
ctx.setTrackerID(.....)

然後這個AccountService 調用了其他Java類,不知道經過了多少層調用以後,最終來到了一個叫做AccountUtil的地方,在這個類中需要使用Context中的trackerID來做點兒事情: 在這裏插入圖片描述

很明顯,這個AccountUtil沒有辦法拿到Context對象, 怎麼辦?

張大胖想到,要不把Context對象一層層地傳遞下去,這樣AccountUtil不就可以得到了嗎? 在這裏插入圖片描述 可是這麼做改動量太大!涉及到的每一層函數調用都得改動,有很多類都不屬於自己的小組管理,還得和別人協調。

更要命的是有些類根本就沒有源碼,想改都改不了。

這也難不住我,張大胖想:可以把那個set/get TrackerID的方法改成靜態(static)的,這樣不管跨多少層調用都沒有問題!

public class Context{
    public static String getTrackerID(){
        ......
    }
    public static void setTrackerID(String id){
        ......
    }
}

在這裏插入圖片描述 這樣就不用一層層地傳遞了,Perfect!

張大胖得意洋洋地把代碼提交給Bill做Review。

Bill看了一眼就指出了致命的問題: 多線程併發的時候出錯!

張大胖恨不得找個地縫鑽進去:又栽在多線程上面了,這次犯的還是低級錯誤!

線程1調用了Context.setTrackerID(), 線程2 也調用了Context.setTrackerID(),數據互相覆蓋,不出亂子纔怪。

張大胖感慨地說:“像我這樣中情況,需要在某處設置一個值,然後經過重重方法調用,到了另外一處把這個值取出來,又要線程安全,實在是不好辦啊, 對了,我能不能把這個值就放到線程中? 讓線程攜帶着這個值到處跑,這樣我無論在任何地方都可以輕鬆獲得了!”

Bill說:“有啊,每個線程都有一個私家領地! 在Thread這個類中有個專門的數據結構,你可以放入你的TrackerID,然後到任何地方都可以把這個TrackerID給取出來。”

“這麼好? ”

張大胖打開JDK中的Thread類,仔細查看,果然在其中有個叫做threadLocals的變量,還是個Map類型 , 但是在Thread類中卻沒有對這個變量操作的方法。

看到張大胖的疑惑,Bill說:“也許你注意到了,這個變量不是通過Thread的訪問的,對他的訪問委託給了ThreadLocal這個類。”

“那我怎麼使用它?”

“非常簡單, 你可以輕鬆創建一個ThreadLocal類的實例:

ThreadLocal<String> threadLocalA= new ThreadLocal<String>();

線程1: threadLocalA.set("1234");
線程2: threadLocalA.set("5678");

像‘1234’, ‘5678’這些值都會放到自己所屬的線程對象中。” 在這裏插入圖片描述

“等你使用的時候,可以這麼辦:” 線程1: threadLocalA.get() --> "1234" 線程2: threadLocalA.get() --> "5678"

“明白了,相當於把各自的數據放入到了各自Thread這個對象中去了,每個線程的值自然就區分開了。 可是我不明白的是爲什麼那個數據結構是個map 呢?”

“你想想,假設你創建了另外一個threadLocalB:”

ThreadLocal<Integer> threadLocalB = new ThreadLocal<Integer>();

線程1: threadLocalB.set(30);
線程2: threadLocalB.set(40);

那線程對象的Map就起到作用了: 在這裏插入圖片描述

“明白了,這個私家領地還真是好用,我現在就把我那個Context給改了,讓它使用ThreadLocal:”

public class Context {
    private static final ThreadLocal<String> mThreadLocal 
        = new ThreadLocal<String>();

    public static void setTrackerID(String id) {
        mThreadLocal.set(id); 
    }   
    public static String getTrackerID() {
        return mThreadLocal.get();
    }    
}

深入ThreadLocal源碼:

ThreadLocalMap是ThreadLocal的匿名內部類,是實現ThreadLocal的關鍵;

ThreadLocalMap其內部利用Entry來實現key-value的存儲,如下: 在這裏插入圖片描述 從上面可以看出:

  • ThreadLocalMap是屬於ThreadLocald匿名內部類
  • Entry是ThreadLocalMap的匿名內部類
  • Entry的key就是ThreadLocal(同時這裏說明了ThreadLocal實例本身是不存儲值,它只是提供了一個在當前線程中找到副本值得key),而value就是具體的值
  • Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal實例)的引用爲一個弱引用(關於弱引用後續會出一篇文章講解一下,請持續關注)

ThreadLocalMap的源碼比較多,我們就看兩個最核心的方法吧!

  • set(ThreadLocal key, Object value)
  • getEntry()

set(ThreadLocal key, Object value)

      /**
         * 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;
            // 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置
            int i = key.threadLocalHashCode & (len-1);

			// 採用“線性探測法”,尋找合適位置(解決hash衝突的一種方法)
            for (Entry e = tab[i];
                 e != null;
                 //查找相鄰的曹
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

				//判斷: key 存在,直接覆蓋
                if (k == key) {
                    e.value = value;
                    return;
                }
              // key == null,但是存在值(因爲此處的e != null)
              //說明之前的ThreadLocal對象已經被回收了
                if (k == null) {
                   // 用新元素替換陳舊的元素
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			// ThreadLocal對應的key實例不存在也沒有陳舊元素,new 一個
            tab[i] = new Entry(key, value);
            int sz = ++size;
                   
			// cleanSomeSlots 清除老舊的Entry(key == null)      
			// 如果沒有清理陳舊的 Entry 並且數組中的元素大於了閾值,則進行 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

這裏的set()操作和我們在集合類的put()方式不一樣的,雖然他們都是key-value結構,不同在於他們解決散列衝突的方式不同。

  • 在集合類中Map的put()採用的是拉鍊法
  • ThreadLocalMap的set()則是採用開放定址法

set()操作除了存儲元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的實例,防止內存泄漏。

set()方法中還有一個很重要變量:threadLocalHashCode,定義如下:

private final int threadLocalHashCode = nextHashCode();

從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的散列值,定義爲final,表示ThreadLocal一旦創建其散列值就已經確定了,生成過程則是調用nextHashCode():

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

nextHashCode表示分配下一個ThreadLocal實例的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal實例的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。

getEntry()

/**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

由於採用了開放定址法,所以當前key的散列值和元素在數組的索引並不是完全對應的,首先取一個探測數(key的散列值),如果所對應的key就是我們所要找的元素,則返回,否則調用getEntryAfterMiss(),如下:

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

這裏有一個重要的地方,當key == null時,調用了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免內存泄漏。

看完了ThreadLocal的ThreadLocalMap,我們再來看看ThreadLocal的set()和get(), initialValue()方法;

get()

返回當前線程所對應的線程變量

    /**
     * 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();
    }

首先通過當前線程獲取所對應的成員變量ThreadLocalMap,然後通過ThreadLocalMap獲取當前ThreadLocal的Entry,最後通過所獲取的Entry獲取目標值result。

getMap()方法可以獲取當前線程所對應的ThreadLocalMap,如下:

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

set(T value)

設置當前線程的線程局部變量的值。

 /**
     * 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);
    }

獲取當前線程所對應的ThreadLocalMap,如果不爲空,則調用ThreadLocalMap的set()方法,key就是當前ThreadLocal,如果不存在,則調用createMap()方法新建一個,如下:

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

initialValue()

返回該線程局部變量的初始值。

    protected T initialValue() {
        return null;
    }
  • 該方法定義爲protected級別且返回爲null,很明顯是要子類實現它的(子類的修飾範圍要大於父類的修飾符),所以我們在使用ThreadLocal的時候一般都應該覆蓋該方法。
  • 該方法不能顯示調用,只有在第一次調用get()或者set()方法時纔會被執行,並且僅執行1次。

remove()

將當前線程局部變量的值刪除。
    //該方法的目的是減少內存的佔用。當然,我們不需要顯示調用該方法
    //因爲一個線程結束後,它所對應的局部變量就會被垃圾回收。
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocal爲什麼會內存泄漏

前面提到每個Thread都有一個ThreadLocal.ThreadLocalMap的map,該map的key爲ThreadLocal實例,它爲一個弱引用,我們知道弱引用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,但是value卻不一定能夠被回收,因爲他還與Current Thread存在一個強引用關係,如下(圖片來自http://www.jianshu.com/p/ee8c9dccc953): 在這裏插入圖片描述 因爲map中存在這個強引用關係,這就會會導致這個value無法被回收。如果這個線程對象不被銷燬,那麼這個強引用關係會一直存在,然後就會出現內存泄漏情況了。所以說只要這個線程對象能夠及時被GC回收,就不會出現內存泄漏。如果碰到線程池,那問題就更大了。

那這樣要怎麼避免這個問題呢?

在前面提過,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情況,會對value設置爲null。當然我們也可以顯示調用ThreadLocal的remove()方法進行處理。

小結

下面再對ThreadLocal進行簡單的總結:

  • ThreadLocal 不是用於解決共享變量的問題的,也不是爲了協調線程同步而存在,而是爲了方便每個線程處理自己的狀態而引入的一個機制。這點至關重要。

  • 每個Thread內部都有一個ThreadLocal.ThreadLocalMap類型的成員變量,該成員變量用來存儲實際的ThreadLocal變量副本。

  • ThreadLocal並不是爲線程保存對象的副本,它僅僅只起到一個索引的作用。它的主要木得視爲每一個線程隔離一個類的實例,這個實例的作用範圍僅限於線程內部。

 

參考來源:https://mp.weixin.qq.com/s/L_5WPcX5pzM8GmTpQbytzw

                       https://mp.weixin.qq.com/s/k4cMqePHagb15-jlYh4PkA            

​關注微信公衆號【悟能之能】瞭解更多編程技巧。

 

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