首頁 > 軟體

ThreadLocal導致JVM記憶體漏失原因探究

2023-10-16 22:00:42

為什麼要使用ThreadLocal

在一整個業務邏輯流程中,為了在不同的地方或者不同的方法中使用同一個物件,但是又不想在方法形參中加這個物件,那麼就可以使用ThreadLocal來儲存

ThreadLocal最大的應用場景就是跨方法進行引數傳遞

ThreadLocal可以給每一個執行緒繫結一個變數的副本

使用ThreadLocal

ThreadLocal常用的方法其實也就下面幾個

// 返回當前執行緒所對應的執行緒區域性變數。
public T get() {}
// 設定當前執行緒的執行緒區域性變數的值。
public void set(T value) {}
// 移除,當執行緒結束後,該執行緒thread物件中的區域性變數將在下一次gc時回收,如果顯示的呼叫此方法只是可以加快記憶體回收的速度
// 所以javase開發 普通new Thread()方式中,這個方法並不是必須要呼叫的
// 但是javaWeb開發中就必須顯示呼叫,因為javaweb都是使用的執行緒池,並不是一個使用者端來一個請求,thread執行緒物件用完就刪除,而是會放回執行緒池中。
public void remove() {}
// 返回該執行緒區域性變數的一個初始化
// protected方法,顯然是為了讓子類覆蓋而設計的。這個方法在第一次呼叫 get()或 set(Object)時才執行,並且僅執行 1 次
protected T initialValue() {}

在具體使用的時候,我們ThreadLocal物件一定會定義成靜態的,如果不定義成靜態的那麼其他地方如何通過這個ThreadLocal範例去Map中拿資料嘞?

而且如果是多個執行緒儲存一個變數的副本,一個靜態的ThreadLocal也足夠了,因為它是作為多個map中的key存在的

簡單使用案例

/**
 * @Description: 在一個方法中呼叫set()方法存值,在另一個方法中呼叫get()方法取值
 */
public class UseThreadLocalTest {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    /**
     * 建立一個執行緒類
     */
    public static class ThreadTest extends Thread{
        private Integer id;
        ThreadTest(Integer id){
            this.id = id;
        }
        @Override
        public void run() {
            threadLocal.set(Thread.currentThread().getName() + ":" + id);
            print();
        }
        public void print(){
            System.out.println(threadLocal.get());
        }
    }
    /**
     * 開三個執行緒
     */
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new ThreadTest(i).start();
        }
    }
}

// 輸入結果如下
Thread-0:0
Thread-1:1
Thread-2:2

具體實現

ThreadLocal底層set()和get()方法的原始碼如下

// 存值時 map最終是儲存在當前執行緒Thread t = Thread.currentThread()中的,是thread的一個成員變數
// map的key是當前threadLocal物件範例,value是要存的值
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物件中取出map
// 然後在從map中根據當前threadLocal物件範例作為key獲取到entry物件
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();
}

為了提高效能,才沒有採用加鎖的方式,而是將map和各個執行緒thread物件進行關聯,這樣就避免了產生執行緒安全問題,也避免了加鎖,提高了效能

我們接下來再來看看ThreadLocalMap它的實現,它類似於jdk1.7版本的hashmap,底層儲存的是一個Entry物件的陣列,初始容量也是16,存值時先用hash結果和陣列長度取餘得到陣列下標位置,然後判斷是否產生了hash衝突,然後使用開發定址法來處理。根據演演算法的不同又可以分為線性探測再雜湊、二次探測再雜湊、偽隨機探測再雜湊。ThreadLocalMap它是使用的線性探測再雜湊法,如下所示

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

Entry物件中的key它是一個弱參照,Entry繼承了WeakReference類,弱參照跟沒參照差不多,GC會直接回收掉,不管記憶體是否足夠都會回收

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

引發記憶體漏失的原因

上面再介紹ThreadLocal基本使用api方法的時候也提到了,如果只是建立一個普通的執行緒Thread物件,是不會產生記憶體漏失問題的。因為map是儲存在Thread物件中,一個普通執行緒執行完了,那麼這個執行緒的區域性變數也就會被gc回收。

但如果結合到了執行緒池,一個Thread執行緒物件用完後放回執行緒池中,如果這個時候我們程式不顯示的呼叫remove()方法,那麼就會造成記憶體漏失問題了。

因為Entry物件中的Key的弱參照,但是value還會存在,就會存在map中key為null的value

ThreadLocal 的底層實現中我們可以看見,無論是 get()set()在某些時 候,呼叫了 expungeStaleEntry() 方法用來清除 Entry 中 Key 為 null 的 Value,但是這是不及時的,也不是每次都會執行的,所以一些情況下還是會發生記憶體洩露。

到此這篇關於ThreadLocal導致JVM記憶體漏失原因探究的文章就介紹到這了,更多相關JVM記憶體漏失內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com