1. 程式人生 > >關於ThreadLocal的實現原理以及ThreadLocal為什麼會造成記憶體洩露

關於ThreadLocal的實現原理以及ThreadLocal為什麼會造成記憶體洩露

用處

可以私有化儲存執行緒的變數值

用法

static class ResourceClass {

        public final static ThreadLocal<String> RESOURCE_1 =
                                       new ThreadLocal<String>();

        public final static ThreadLocal<String> RESOURCE_2 =
                                       new
ThreadLocal<String>(); } static class A { public void setOne(String value) { ResourceClass.RESOURCE_1.set(value); } public void setTwo(String value) { ResourceClass.RESOURCE_2.set(value); } } static class B { public
void display() { System.out.println(ResourceClass.RESOURCE_1.get() + ":" + ResourceClass.RESOURCE_2.get()); } } public static void main(String []args) { final A a = new A(); final B b = new B(); for(int i = 0 ; i < 15 ; i ++) { final String resouce1 = "執行緒-"
+ I; final String resouce2 = " value = (" + i + ")"; new Thread() { public void run() { try { a.setOne(resouce1); a.setTwo(resouce2); b.display(); }finally { ResourceClass.RESOURCE_1.remove(); ResourceClass.RESOURCE_2.remove(); } } }.start(); } }

執行結果

執行緒-4: value = (4)
執行緒-2: value = (2)
執行緒-5: value = (5)
執行緒-1: value = (1)
執行緒-0: value = (0)
執行緒-3: value = (3)
執行緒-6: value = (6)
執行緒-7: value = (7)
執行緒-10: value = (10)
執行緒-11: value = (11)
執行緒-9: value = (9)
執行緒-12: value = (12)
執行緒-13: value = (13)
執行緒-8: value = (8)
執行緒-14: value = (14)

可以看到執行緒是交替執行的,但是每個執行緒取到的值卻沒有亂,說明已經做到了執行緒的私有化。

實現原理

看ThreadLocal的set方法原始碼

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//獲取當前執行緒的ThreadLocalMap
        if (map != null)
            map.set(this, value);//以當前ThreadLocal物件為鍵,value為值儲存到map中
        else
            createMap(t, value);
    }

ThreadLocal的get方法原始碼

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//獲取當前執行緒的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//以當前ThreadLocal物件為鍵,獲取到map中的物件
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

通過上述原始碼可以看到,實際上是每個Thread執行緒維護著一個ThreadLocalMap,而ThreadLocalMap中的一個鍵即是一個ThreadLocal,value是我們通過set方法傳進去的引數。看下面的圖會好理解一點
這裡寫圖片描述
也就是說 ThreadLocal 本身並不儲存值,它只是作為一個 key 來讓執行緒從 ThreadLocalMap 獲取 value。

關於記憶體洩漏

為什麼會造成記憶體洩漏

下面字字精華,請耐心讀完
我們上面說了ThreadLocal 本身並不儲存值,它只是作為一個 key儲存到ThreadLocalMap中,但是這裡要注意的是它作為一個key用的是弱引用(什麼是強引用,軟引用,弱引用,虛引用)本處不做闡述。
因為沒有強引用鏈,弱引用在GC的時候可能會被回收。這樣就會在ThreadLocalMap中存在一些key為null的鍵值對(Entry)。因為key變成null了,我們是沒法訪問這些Entry的,但是這些Entry本身是不會被清除的,為什麼呢?因為存在一條強引用鏈。即執行緒本身->ThreadLocalMap->Entry也就是說,恰恰我們在使用執行緒池的時候,執行緒使用完了是會放回到執行緒池迴圈使用的。由於ThreadLocalMap的生命週期和執行緒一樣長,如果沒有手動刪除對應key就會導致這塊記憶體即不會回收也無法訪問,也就是記憶體洩漏。
其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除執行緒ThreadLocalMap裡所有key為null的value。但是這些舉動不能保證記憶體就一定會回收,因為可能這條執行緒被放回到執行緒池裡後再也沒有使用,或者使用的時候沒有呼叫其get(),set(),remove()方法。

為什麼使用弱引用,記憶體洩漏是否是弱引用的鍋?

下面我們分兩種情況討論:

(1)key 使用強引用:引用的ThreadLocal的物件被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry記憶體洩漏。

(2)key 使用弱引用:引用的ThreadLocal的物件被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap呼叫set、get、remove的時候會被清除。

因此:記憶體洩漏歸根結底是由於ThreadLocalMap的生命週期跟Thread一樣長。如果沒有手動刪除對應key就會導致記憶體洩漏,而不是因為弱引用。

如何避免記憶體洩漏

每次使用完ThreadLocal,都呼叫它的remove()方法,清除資料。
注意:並不是所有使用ThreadLocal的地方,都在最後remove(),他們的生命週期可能是需要和專案的生存週期一樣長的,所以要進行恰當的選擇,以免出現業務邏輯錯誤!

寫在最後

看來好像沒有什麼更好的辦法能避免這個問題,小生不才,希望能有指正。

參考原文:https://blog.csdn.net/bntX2jSQfEHy7/article/details/78315161