1. 程式人生 > >ThreadLocal理解及應用

ThreadLocal理解及應用

     本次給大家介紹重要的工具ThreadLocal。講解內容如下,同時介紹什麼場景下發生記憶體洩漏,如何復現記憶體洩漏,如何正確使用它來避免記憶體洩漏。

  1. ThreadLocal是什麼?有哪些用途?
  2. ThreadLocal如何使用
  3. ThreadLocal原理
  4. ThreadLocal使用有哪些坑及注意事項

1. ThreadLocal是什麼?有哪些用途?

    首先介紹Thread類中屬性threadLocals:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */


ThreadLocal.ThreadLocalMap threadLocals = null;
    我們發現Thread並沒有提供成員變數threadLocals的設定與訪問的方法,那麼每個執行緒的例項threadLocals引數我們如何操作呢?這時我們的主角:ThreadLocal就登場了。
  所以有那麼一句總結:ThreadLocal是執行緒Thread中屬性threadLocals的管理者
也就是說我們對於ThreadLocal的get, set,remove的操作結果都是針對當前執行緒Thread例項的threadLocals存,取,刪除操作。類似於一個開發者的任務,產品經理左右不了,產品經理只能通過技術leader來給開發者分配任務。下面再舉個栗子,進一步說明他們之間的關係:
在這裡插入圖片描述

1.每個人都一張銀行卡
2.每個人每張卡都有一定的餘額。
3.每個人獲取銀行卡餘額都必須通過該銀行的管理系統。
4.每個人都只能獲取自己卡持有的餘額資訊,他人的不可訪問。

在這裡插入圖片描述
對映到我們要說的ThreadLocal
1.card類似於Thread
2.card餘額屬性,卡號屬性等類似於Treadlocal內部屬性集合threadLocals
3.cardManager類似於ThreadLocal管理類

那ThreadLocal有哪些應用場景呢?
    其實我們無意間已經時時刻刻在使用ThreadLocal提供的便利,如果說多資料來源的切換你比較陌生,那麼spring提供的宣告式事務就再熟悉不過了,我們在研發過程中無時無刻不在使用,而spring宣告式事務的重要實現基礎就是ThreadLocal,只不過大家沒有去深入研究spring宣告式事務的實現機制。後面有機會我會給大家介紹spring宣告式事務的原理及實現機制。
    原來ThreadLocal

這麼強大,但應用開發者使用較少,同時有些研發人員對於ThreadLocal記憶體洩漏,等潛在問題,不敢試用,恐怕這是對於ThreadLocal最大的誤解,後面我們將會仔細分析,只要按照正確使用方式,就沒什麼問題。如果ThreadLocal存在問題,豈不是spring宣告式事務是我們程式最大的潛在危險嗎?

2.ThreadLocal如何使用

    為了更直觀的體會ThreadLocal的使用我們假設如下場景
1.我們給每個執行緒生成一個ID。
2.一旦設定,執行緒生命週期內不可變化。
3.容器活動期間不可以生成重複的ID

我們建立一個ThreadLocal管理類:
這裡寫圖片描述
測試程式如下:我們同一個執行緒不斷get,測試id是否變化,同時測試完成後我們就將其釋放掉。
這裡寫圖片描述
在主程式中我們開啟多個執行緒測試不通執行緒之間是否會影響
這裡寫圖片描述
不出意外我們的結果為:
這裡寫圖片描述
結果:確實是不同執行緒間id不同,相同執行緒id相同。

3.ThreadLocal原理

①ThreadLocal類結構及方法解析:
在這裡插入圖片描述
上圖可知:ThreadLocal三個方法get, set , remove以及內部類`ThreadLocalMap

②ThreadLocal及Thread之間的關係:
在這裡插入圖片描述
從這張圖我們可以直觀的看到Thread中屬性threadLocals,作為一個特殊的Map,它的key值就是我們ThreadLocal例項,而value值這是我們設定的值。

③ThreadLocal的操作過程:
我們以get方法為例:
在這裡插入圖片描述
其中getMap(t)返回的就上當前執行緒的threadlocals,如下圖,然後根據當前ThreadLocal例項物件作為key獲取ThreadLocalMap中的value,如果首次進來這呼叫setInitialValue()
在這裡插入圖片描述

在這裡插入圖片描述
set的過程也類似:
在這裡插入圖片描述

注意:ThreadLocal中可以直接t.threadLocals是因為ThreadThreadLocal在同一個包下,同樣Thread可以直接訪問ThreadLocal.ThreadLocalMap threadLocals = null;來進行宣告屬性。

4.ThreadLocal使用有哪些坑及注意事項

    我經常在網上看到駭人聽聞的標題,ThreadLocal導致記憶體洩漏,這通常讓一些剛開始對ThreadLocal理解不透徹的開發者,不敢貿然使用。越不用,越陌生。這樣就讓我們錯失了更好的實現方案,所以敢於引入新技術,敢於踩坑,才能不斷進步。
我們來看下為什麼說ThreadLocal會引起記憶體洩漏,什麼場景下會導致記憶體洩漏?
    先回顧下什麼叫記憶體洩漏,對應的什麼叫記憶體溢位
①Memory overflow:記憶體溢位,沒有足夠的記憶體提供申請者使用。
②Memory leak:記憶體洩漏,程式申請記憶體後,無法釋放已申請的記憶體空間,記憶體洩漏的堆積終將導致記憶體溢位。
顯然是TreadLocal在不規範使用的情況下導致了記憶體沒有釋放。
在這裡插入圖片描述
紅框裡我們看到了一個特殊的類WeakReference,同樣這個類,應用開發者也同樣很少使用,這裡簡單介紹下吧

型別 回收時間 應用場景
強引用 一直存活,除非GC Roots不可達 所有程式的場景,基本物件,自定義物件等
軟引用 記憶體不足時會被回收 一般用在對記憶體非常敏感的資源上,用作快取的場景比較多,例如:網頁快取、圖片快取
弱引用 只能存活到下一次GC前 生命週期很短的物件,例如ThreadLocal中的Key。
虛引用 隨時會被回收, 建立了可能很快就會被回收 可能被JVM團隊內部用來跟蹤JVM的垃圾回收活動

    既然WeakReference在下一次gc即將被回收,那麼我們的程式為什麼沒有出問題呢?
    ①所以我們測試下弱引用的回收機制:

在這裡插入圖片描述

這一種存在強引用不會被回收。
在這裡插入圖片描述
這裡沒有強引用將會被回收。

上面演示了弱引用的回收情況,下面我們看下ThreadLocal的弱引用回收情況。
    ②ThreadLocal的弱引用回收情況
在這裡插入圖片描述
如上圖所示,我們在作為key的ThreadLocal物件沒有外部強引用,下一次gc必將產生key值為null的資料,若執行緒沒有及時結束必然出現,一條強引用鏈
Threadref–>Thread–>ThreadLocalMap–>Entry,所以這將導致記憶體洩漏。

下面我們模擬復現ThreadLocal導致記憶體洩漏:
1.為了效果更佳明顯我們將我們的treadlocals的儲存值value設定為1萬字符串的列表:

class ThreadLocalMemory {
    // Thread local variable containing each thread's ID
    public ThreadLocal<List<Object>> threadId = new ThreadLocal<List<Object>>() {
        @Override
        protected List<Object> initialValue() {
            List<Object> list = new ArrayList<Object>();
            for (int i = 0; i < 10000; i++) {
                list.add(String.valueOf(i));
            }
            return list;
        }
    };
    // Returns the current thread's unique ID, assigning it if necessary
    public List<Object> get() {
        return threadId.get();
    }
    // remove currentid
    public void remove() {
        threadId.remove();
    }
}

測試程式碼如下:

public static void main(String[] args)
            throws InterruptedException {

        //  為了復現key被回收的場景,我們使用臨時變數
        ThreadLocalMemory memeory = new ThreadLocalMemory();

        // 呼叫
        incrementSameThreadId(memeory);

        System.out.println("GC前:key:" + memeory.threadId);
        System.out.println("GC前:value-size:" + refelectThreadLocals(Thread.currentThread()));

        // 設定為null,呼叫gc並不一定觸發垃圾回收,但是可以通過java提供的一些工具進行手工觸發gc回收。
        memeory.threadId = null;
        System.gc();

        System.out.println("GC後:key:" + memeory.threadId);
        System.out.println("GC後:value-size:" + refelectThreadLocals(Thread.currentThread()));

        // 模擬執行緒一直執行
        while (true) {
        }
    }

此時我們如何知道記憶體中存在memory leak呢?
我們可以藉助jdk提供的一些命令dump當前堆記憶體,命令如下:
jmap -dump:live,format=b,file=heap.bin <pid>
然後我們藉助MAT視覺化分析工具,來檢視對記憶體,分析物件例項的存活狀態:
在這裡插入圖片描述
在這裡插入圖片描述
首先開啟我們工具提示我們的記憶體洩漏分析:
在這裡插入圖片描述
    這裡我們可以確定的是ThreadLocalMap例項的Entry.value是沒有被回收的。
    最後我們要確定Entry.key是否還在?開啟Dominator Tree,搜尋我們的ThreadLocalMemory,發現並沒有存活的例項。
在這裡插入圖片描述
在這裡插入圖片描述
以上我們復現了ThreadLocal不正當使用,引起的記憶體洩漏。demo在這裡
    所以我們總結了使用ThreadLocal時會發生記憶體洩漏的前提條件:
ThreadLocal引用被設定為null,且後面沒有set,get,remove操作。
②執行緒一直執行,不停止。(執行緒池)
③觸發了垃圾回收。(Minor GC或Full GC)
    我們看到ThreadLocal出現記憶體洩漏條件還是很苛刻的,所以我們只要破壞其中一個條件就可以避免記憶體洩漏,單但為了更好的避免這種情況的發生我們使用ThreadLocal時遵守以下兩個小原則:
    ①ThreadLocal申明為private static final。
         Private與final 儘可能不讓他人修改變更引用,
         Static 表示為類屬性,只有在程式結束才會被回收。
    ②ThreadLocal使用後務必呼叫remove方法。
        最簡單有效的方法是使用後將其移除。

以上。