ThreadLocal刨根問底

MXC肖某某發表於2020-08-04

一、ThreadLocal使用場景

  在資料庫使用connection物件時,每個客戶都能使用自己的connection物件,防止出現客戶ClientA操作關閉ClientB的connection連線物件。

  案例:https://zhuanlan.zhihu.com/p/82737256

二、ThreadLocal中的remove()使用

  1,防止記憶體洩露

  2,執行緒不安全

    在ThreadLocal和執行緒池聯合使用的時候,會出現下個業務請求複用到上一個執行緒的情況,導致使用相同的ThreadLocal執行不同的業務邏輯。

public class ThreadLocalAndPool {
    private static ThreadLocal<Integer> variableLocal = ThreadLocal.withInitial(() -> 0);
    public static int get() {
        return variableLocal.get();
    }
    public static void remove() {
        variableLocal.remove();
    }
    public static void increment() {
        variableLocal.set(variableLocal.get() + 1);
    }

    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(12));
        for (int i = 0; i < 5; i++) {
            executorService.execute(() -> {
                long threadId = Thread.currentThread().getId();

                int before = get();
                increment();
                int after = get();
                System.out.println("threadid " + threadId + "  before " + before + ", after " + after);
            });
        }
        executorService.shutdown();
    }
}
threadid 12  before 0, after 1
threadid 13  before 0, after 1
threadid 12  before 1, after 2
threadid 13  before 1, after 2
threadid 12  before 2, after 3

三、為什麼會出現記憶體洩露?

  ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次JVM垃圾收集時被回收。這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前執行緒的生命週期很長,一直存在,那麼其內部的ThreadLocalMap物件也一直生存下來,這些null key就存在一條強引用鏈的關係一直存在:Thread --> ThreadLocalMap-->Entry(table)-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成記憶體洩漏。

四、為什麼使用弱引用?

  從表面上看,發生記憶體洩漏,是因為Key使用了弱引用型別。但其實是因為整個Entry的key為null後,沒有主動清除value導致。為什麼使用弱引用而不是強引用?

官方文件的說法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了處理非常大和生命週期非常長的執行緒,雜湊表使用弱引用作為 key。

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

key 使用強引用:引用的ThreadLocal的物件被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry記憶體洩漏。
key 使用弱引用:引用的ThreadLocal的物件被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap呼叫set,get,remove的時候會被清除。

  比較兩種情況,我們可以發現:由於ThreadLocalMap的生命週期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致記憶體洩漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會記憶體洩漏,對應的value在下一次ThreadLocalMap呼叫set,get,remove的時候會被清除。

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

相關文章