Threadlocal詳解,很詳細了

什麼是 ThreadLocal?

ThreadLocal 誕生於 JDK 1.2,用於解決多線程間的數據隔離問題。也就是說 ThreadLocal 會爲每一個線程創建一個單獨的變量副本。

ThreadLocal 有什麼用?

ThreadLocal 最典型的使用場景有兩個:

  • ThreadLocal 可以用來管理 Session,因爲每個人的信息都是不一樣的,所以就很適合用 ThreadLocal 來管理;

  • 數據庫連接,爲每一個線程分配一個獨立的資源,也適合用 ThreadLocal 來實現。

其中,ThreadLocal 也被用在很多大型開源框架中,比如 Spring 的事務管理器,還有 Hibernate 的 Session 管理等,既然 ThreadLocal 用途如此廣泛,那接下來就讓我們共同看看 ThreadLocal 要怎麼用?ThreadLocal 使用中要注意什麼?以及 ThreadLocal 的存儲原理等,一起來看吧。

ThreadLocal 使用

ThreadLocal 基本使用

ThreadLocal 常用方法有 set(T)、get()、remove() 等,具體使用請參考以下代碼。

ThreadLocal threadLocal = new ThreadLocal();
// 存值
threadLocal.set(Arrays.asList("老王", "Java 面試題"));
// 取值
List list = (List) threadLocal.get();
System.out.println(list.size());
System.out.println(threadLocal.get());
//刪除值
threadLocal.remove();
System.out.println(threadLocal.get());

以上程序執行結果如下:

2

[老王, Java 面試題]

null

ThreadLocal 所有方法,如下圖所示:

ThreadLocal 數據共享

既然 ThreadLocal 設計的初衷是解決線程間信息隔離的,那 ThreadLocal 能不能實現線程間信息共享呢?

答案是肯定的,只需要使用 ThreadLocal 的子類 InheritableThreadLocal 就可以輕鬆實現,來看具體實現代碼:

ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("老王");
new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();

以上程序執行結果如下:

老王

從以上代碼可以看出,主線程和新創建的線程之間實現了信息共享。

ThreadLocal 內存溢出

內存溢出

下面我們用代碼實現 ThreadLocal 內存溢出的情況,請參考以下代碼。

class ThreadLocalTest {
    static ThreadLocal threadLocal = new ThreadLocal();
    static Integer MOCK_MAX = 10000;
    static Integer THREAD_MAX = 100;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
        for (int i = 0; i < THREAD_MAX; i++) {
            executorService.execute(() -> {
                threadLocal.set(new ThreadLocalTest().getList());
                System.out.println(Thread.currentThread().getName());
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
    List getList() {
        List list = new ArrayList();
        for (int i = 0; i < MOCK_MAX; i++) {
            list.add("Version:JDK 8");
            list.add("ThreadLocal");
            list.add("Author:老王");
            list.add("DateTime:" + LocalDateTime.now());
            list.add("Test:ThreadLocal OOM");
        }
        return list;
    }
}

設置 JVM(Java 虛擬機)啓動參數 -Xmx=100m (最大運行內存 100 M),運行程序不久後就會出現如下異常:

此時我們用 VisualVM 觀察到程序運行的內存使用情況,發現內存一直在緩慢地上升直到內存超出最大值,從而發生內存溢出的情況。

內存使用情況,如下圖所示:

內存溢出原理分析

在開始之前,先來看下 ThreadLocal 是如何存儲數據的。

首先,找到 ThreadLocal.set() 的源碼,代碼如下(此源碼基於 JDK 8):

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

可以看出 ThreadLocal 首先獲取到 ThreadLocalMap 對象,然後再執行 ThreadLocalMap.set() 方法,進而打開此方法的源碼,代碼如下:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

從整個代碼可以看出,首先 ThreadLocal 並不存儲數據,而是依靠 ThreadLocalMap 來存儲數據,ThreadLocalMap 中有一個 Entry 數組,每個 Entry 對象是以 K/V 的形式對數據進行存儲的,其中 K 就是 ThreadLocal 本身,而 V 就是要存儲的值,如下圖所示:

可以看出:一個 Thread 中只有一個 ThreadLocalMap,每個 ThreadLocalMap 中存有多個 ThreadLocal,ThreadLocal 引用關係如下:

image.png

其中:實線代表強引用,虛線代表弱引用(弱引用具有更短暫的生命週期,在執行垃圾回收時,一旦發現只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存)。

看到這裏我們就理解了 ThreadLocal 造成內存溢出的原因:如果 ThreadLocal 沒有被直接引用(外部強引用),在 GC(垃圾回收)時,由於 ThreadLocalMap 中的 key 是弱引用,所以一定就會被回收,這樣一來 ThreadLocalMap 中就會出現 key 爲 null 的 Entry,並且沒有辦法訪問這些數據,如果當前線程再遲遲不結束的話,這些 key 爲 null 的 Entry 的 value 就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 並且永遠無法回收,從而造成內存泄漏。

ThreadLocal 的正確使用方法

既然已經瞭解了 ThreadLocal 內存溢出的原因,那解決的辦法就很簡單了,只需要在使用完 ThreadLocal 之後,把內容刪除掉(remove)就可以,因此 ThreadLocal 完整的正確使用代碼如下:

class ThreadLocalTest {
    static ThreadLocal threadLocal = new ThreadLocal();
    static Integer MOCK_MAX = 10000;
    static Integer THREAD_MAX = 100;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
        for (int i = 0; i < THREAD_MAX; i++) {
            executorService.execute(() -> {
                threadLocal.set(new ThreadLocalTest().getList());
                System.out.println(Thread.currentThread().getName());
                // 移除對象
                threadLocal.remove(); 
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
    List getList() {
        List list = new ArrayList();
        for (int i = 0; i < MOCK_MAX; i++) {
            list.add("Version:JDK 8");
            list.add("ThreadLocal");
            list.add("Author:老王");
            list.add("DateTime:" + LocalDateTime.now());
            list.add("Test:ThreadLocal OOM");
        }
        return list;
    }
}

可以看出核心代碼,我們添加了一句 threadLocal.remove() 命令就解決了內存溢出的問題,這個時候運行代碼觀察,發現內存的值一直在一個固定的範圍內,如下圖所示:

這樣就解決了 ThreadLocal 內存溢出的問題了。

相關面試題

1.ThreadLocal 爲什麼是線程安全的?

答:ThreadLocal 爲每一個線程維護變量的副本,把共享數據的可見範圍限制在同一個線程之內,因此 ThreadLocal 是線程安全的,每個線程都有屬於自己的變量。

2.ThreadLocal 如何共享數據?

答:通過 ThreadLocal 的子類 InheritableThreadLocal 可以天然的支持多線程間的信息共享。

3.以下程序打印的結果是 true 還是 false?

ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("老王");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("老王");
new Thread(() -> {
    System.out.println(threadLocal.get().equals(threadLocal2.get()));
}).start();

答:false。
題目分析:因爲 threadLocal 使用的是 InheritableThreadLocal(共享本地線程),所以 threadLocal.get() 結果爲 老王,而 threadLocal2 使用的是 ThreadLocal,因此在新線程中 threadLocal2.get() 的結果爲 null,因而它們比較的最終結果爲 false。

4.ThreadLocal 爲什麼會發生內存溢出?

答:ThreadLocal 造成內存溢出的原因:如果 ThreadLocal 沒有被直接引用(外部強引用),在 GC(垃圾回收)時,由於 ThreadLocalMap 中的 key 是弱引用,所以一定就會被回收,這樣一來 ThreadLocalMap 中就會出現 key 爲 null 的 Entry,並且沒有辦法訪問這些數據,如果當前線程再遲遲不結束的話,這些 key 爲 null 的 Entry 的 value 就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 並且永遠無法回收,從而造成內存泄漏。

5.解決 ThreadLocal 內存溢出的關鍵代碼是什麼?

答:關鍵代碼爲 threadLocal.remove() ,使用完 ThreadLocal 之後,調用remove() 方法,清除掉 ThreadLocalMap 中的無用數據就可以避免內存溢出了。

6.ThreadLocal 和 Synchonized 有什麼區別?

答:ThreadLocal 和 Synchonized 都用於解決多線程併發訪問,防止任務在共享資源上產生衝突,但是 ThreadLocal 與 Synchronized 有本質的區別,Synchronized 用於實現同步機制,是利用鎖的機制使變量或代碼塊在某一時刻只能被一個線程訪問,是一種 “以時間換空間” 的方式;而 ThreadLocal 爲每一個線程提供了獨立的變量副本,這樣每個線程的(變量)操作都是相互隔離的,這是一種 “以空間換時間” 的方式。

總結

ThreadLocal 的主要方法是 set(T) 和 get(),用於多線程間的數據隔離,ThreadLocal 也提供了 InheritableThreadLocal 子類,用於實現多線程間的數據共享。但使用 ThreadLocal 一定要注意用完之後使用 remove() 清空 ThreadLocal,不然會操作內存溢出的問題。

下一篇:線程安全

在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈

往期精選

分佈式數據之緩存技術,一起來揭開其神祕面紗

分佈式數據複製技術,今天就教你真正分身術

數據分佈方式之哈希與一致性哈希,我就是個神算子

分佈式存儲系統三要素,掌握這些就離成功不遠了

想要設計一個好的分佈式系統,必須搞定這個理論

分佈式通信技術之發佈訂閱,乾貨滿滿

分佈式通信技術之遠程調用:RPC

消息隊列Broker主從架構詳細設計方案,這一篇就搞定主從架構

消息中間件路由中心你會設計嗎,不會就來學學

消息隊列消息延遲解決方案,跟着做就行了

秒殺系統每秒上萬次下單請求,我們該怎麼去設計

【分佈式技術】分佈式系統調度架構之單體調度,非掌握不可

CDN加速技術,作爲開發的我們真的不需要懂嗎?

煩人的緩存穿透問題,今天教就你如何去解決

分佈式緩存高可用方案,我們都是這麼幹的

每天百萬交易的支付系統,生產環境該怎麼設置JVM堆內存大小

你的成神之路我已替你鋪好,沒鋪你來捶我

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