面試|再次講解Threadlocal使用及其內存溢出

原文鏈接:https://cloud.tencent.com/developer/article/1459717

浪尖整理本文主要是想幫助大家完全消化面試中常見的ThreadLocal問題。希望讀懂此文以後大家可以掌握(沒耐心的可以直接閱讀底部總結):

  1. 簡單介紹原理
  2. ThreadLocal使用案例場景
  3. Threadlocal的底層原理
  4. Threadlocal內存溢出原因
  5. 解決方法

 

1. 簡介

高併發處理起來比較麻煩,很多新手對此都會非常頭疼。要知道避免併發的最簡單辦法就是線程封閉,也即是把對象封裝到一個線程裏,那麼對象就只會被當前線程能看到,使得對象就算不是線程安全的也不會出現任何安全問題。Threadlocal是實現該策略的最好的方法。Threadlocal爲每個線程提供了一個私有變量,然後線程訪問該變量(get或者set)的時候實際上是讀寫的自己的局部變量從而避免了併發法問題。

 

2. 案例使用

首先定義一個ThreadLocal的封裝工具類

public class Bank {
    /*ThreadLocal<Integer> t = ThreadLocal.withInitial(() -> 100);*/
    ThreadLocal<Integer> t = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 100;
        }
    };

    public int get() {
        return t.get();
    }

    public void set() {
        t.set(t.get() + 10);
    }
}

實現一個Runnable對象然後使用bank對象

public class Transfer implements Runnable {
    private Bank bank;

    public Transfer(Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            bank.set();
            System.out.println(Thread.currentThread() + " : " + bank.get());
        }
    }
}

定義兩個線程t1和t2,運行之後查看結果:

public class App {

    public static void main(String[] args) {
        Bank bank = new Bank();
        Transfer t = new Transfer(bank);
        Thread t1 = new Thread(t);
        t1.start();
        Thread t2 = new Thread(t);
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(bank.get());
    }

}

查看輸出結果就會發現,發現主線程,線程t1,線程t2之間相互不影響~

Thread[Thread-0,5,main] : 110
Thread[Thread-1,5,main] : 110
Thread[Thread-0,5,main] : 120
Thread[Thread-1,5,main] : 120
Thread[Thread-0,5,main] : 130
Thread[Thread-1,5,main] : 130
Thread[Thread-0,5,main] : 140
Thread[Thread-1,5,main] : 140
Thread[Thread-0,5,main] : 150
Thread[Thread-1,5,main] : 150
Thread[Thread-0,5,main] : 160
Thread[Thread-1,5,main] : 160
Thread[Thread-0,5,main] : 170
Thread[Thread-1,5,main] : 170
Thread[Thread-0,5,main] : 180
Thread[Thread-1,5,main] : 180
Thread[Thread-0,5,main] : 190
Thread[Thread-1,5,main] : 190
Thread[Thread-0,5,main] : 200
Thread[Thread-1,5,main] : 200
100

 

3. 底層源碼

每個線程Thread內部都會有ThreadLocal.ThreadLocalMap對象,該對象是一個自定義的map,key是弱引用包裝的ThreadLocal類型,value就是我們的值。

初始值

Threadlocal直接在構造的時候設置初始值。主要是要實現其initialValue方法:

new ThreadLocal<Integer>() {
    @Override
    protected IntegerinitialValue() {
        return 100;
    }
};

追蹤一下該方法,會發現其僅僅被一個私有方法調用了

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

解讀一下setInitialValue私有方法

  1. 首先調用initialVaule方法,獲取初始值。
  2. 然後獲取當前線程對象的引用
  3. 通過線程對象引用獲取ThreadLocal.ThreadLocalMap對象 map。
  4. map對象不爲空,就將當前threadlocal弱引用作爲key,初始值爲value完成初始化。
  5. Map對象爲空,調用createMap方法,並完成初始化。
void createMap(Thread t, T firstValue) {
 t.threadLocals = new ThreadLocalMap(this, firstValue);
}

讀到這可能會很好奇,爲啥只是被私有方法調用,我們又無權調用該私有方法,如何實現初始化呢?也是很簡單的在我們第一次調用get的時候,會調用該私有初始化方法,來真正完成初始化。

Get方法

具體代碼如下:

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

我們來解讀一下get方法,此處就真正暴露ThreadLocal的真實面目了。

  1. 獲取當前線程對象,t
  2. 通過getMap(t)方法來獲取t內部的ThreadLocal.ThreadLocalMap對象。
  3. 然後判斷ThreadLocalMap對象是否爲空,不爲空就可以通過當前Threadlocal對象獲取對應的value值,存在返回,不存在跳過。
  4. 假如map爲空或者當前threadlocal對象對應的value爲空,那麼就調用初始化方法setInitialValue初始化並返回初始值。

Set方法

接下來解讀一下threadlocal變量的set方法。Set的方法源碼如下:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  1. 獲取當前線程對象 t
  2. 通過getMap(t)方法來獲取t內部的ThreadLocal.ThreadLocalMap對象 map。
  3. map不爲空,當前threadlocal對象作爲key(弱引用),要設置的value作爲value完成值的設置。
  4. 假如map爲空,就調用createMap方法,給當前線程創建一個ThreadlocalMap
void createMap(Thread t, T firstValue) {
 t.threadLocals = new ThreadLocalMap(this, firstValue);
}

remove方法

threadlocal的remove方法主要作用是刪除當前threadlocal對應的鍵值對。

public void remove() {
	ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

 

4. 內存泄漏

根據前面對threadlocal的整理,其實可以畫出來一個結構圖:
在這裏插入圖片描述
對value的引用線路有兩條:

  1. threadlocalref 是ThreadLocal強引用,key是ThreadLocal變量的弱引用。由於key是弱引用,當ThreadLocalRef因不用而釋放掉的時候,ThreadLocal對象就會被回收,由於是key到threadLocal對象爲弱引用,一旦進行垃圾回收key就會被回收而相應位置變爲null,當然value依然存在。
  2. 通過當前線程的引用可以獲取當前線程對象,當前線程對象就可以獲取到ThreadLocalMap,那麼只要當前線程一直存在,ThreadLocalMap對象就會一直存在。

由於ThreadlocalMap存活時間和線程一樣,比如我們採用的是常駐線程池,使用線程過程中沒有清空ThreadLocalMap,也沒有調用threadlocal的remove方法,就將線程放回線程池,雖然ThreadLocal的強引用ThreadLocalRef被清除,弱引用key在GC的時候也會被設置爲null,但是對於value值還存在一條強引用鏈條:

currentThreadRef -> currentThread -> ThreadLocalMap -> Entry(value)

所以value並沒有釋放,就造成了內存泄漏了。

那這時候你或許會問爲啥ThreadLocalMap存儲value的時候不採用弱引用呢?這樣不就可以避免內存泄漏了麼?value是弱引用是不行的,原因很簡單:我們存儲的對象除了ThreadLocalMap的Value就沒有其他的引用了,value一但是對象的弱引用,GC的時候被回收,對象就無法訪問了,這顯然不是我們想要的。

 

5. 避免內存泄漏

爲避免內存泄漏最好在使用完ThreadLocal之後調用其remove方法,將數據清除掉。

當然,對於Java8 ThreadLocalMap 的 set 方法通過調用 replaceStaleEntry 方法回收鍵爲 null 的 Entry 對象的值(即爲具體實例)以及 Entry 對象本身從而防止內存泄漏

get方法會間接調用expungeStaleEntry 方法將鍵和值爲 null 的 Entry 設置爲 null 從而使得該 Entry 可被回收

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