ThreadLocal到底是啥

前言

相信很多同學都聽過ThreadLocal,即使沒用過也聽過。但是要仔細一問ThreadLocal是個啥,很多同學也不一定能說清楚。本篇博客就是爲了回答關於ThreadLocal的一系列靈魂拷問:ThreadLocal是個什麼?怎麼用?爲什麼要用它?它有什麼缺點?怎麼避免…

ThreadLoacl

ThreadLoacl是什麼

在瞭解ThreadLocal之前,我們先了解下什麼是線程封閉

把對象封閉在一個線程裏,即使這個對象不是線程安全的,也不會出現併發安全問題。

實現線程封閉大致有三種方式:

  • Ad-hoc線程封閉:維護線程封閉性的職責完全由程序來承擔,不推薦使用
  • 棧封閉:也就是局部變量
    public void testThread() {
        StringBuilder sb = new StringBuilder();
        sb.append("Hello");
    }
    
    StringBuilder是線程不安全的,但是它只是個局部變量,局部變量存儲在虛擬機棧,虛擬機棧是線程隔離的,所以不會有線程安全問題(不瞭解虛擬機分區的同學可以看看我之前的博客:Java虛擬機——內存區域)。
  • ThreadLocal線程封閉:簡單易用

第三種方式就是通過ThreadLocal來實現線程封閉,線程封閉的指導思想是封閉,而不是共享。說ThreadLocal是用來解決變量共享的併發安全問題,多少有些不準確。

使用

JDK1.2開始提供的java.lang.ThreadLocal的使用方式非常簡單

public class ThreadLocalDemo {
    
    public static void main(String[] args) throws InterruptedException {

        final ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("main-thread : Hello");
        
        Thread thread = new Thread(() -> {
            // 獲取不到主線程設置的值,所以爲null
            System.out.println(threadLocal.get());
            threadLocal.set("sub-thread : World");
            System.out.println(threadLocal.get());
        });
        // 啓動子線程
        thread.start();
        // 讓子線程先執行完成,再繼續執行主線
        thread.join();
        // 獲取到的是主線程設置的值,而不是子線程設置的
        System.out.println(threadLocal.get());
        threadLocal.remove();
        System.out.println(threadLocal.get());
    }
}

運行結果

null
sub-thread : World
main-thread : Hello
null

運行結果說明了ThreadLocal只能獲取本線程設置的值,也就是線程封閉。基本上,ThreadLocal對外提供的方法只有三個get()set(T)remove()

原理

使用方式非常簡單,所以我們來看看ThreadLocal的源碼。ThreadLocal內部定義了一個靜態ThreadLocalMap類,ThreadLocalMap內部又定義了一個Entry類,這裏只看一些主要的屬性和方法

public class ThreadLocal<T> {

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

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

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

	// 從這裏可以看出ThreadLocalMap對象是被Thread類持有的
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

	// 內部類ThreadLocalMap
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
			// 內部類Entity,實際存儲數據的地方
			// Entry的key是ThreadLocal對象,不是當前線程ID或者名稱
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
		// 注意這裏維護的是Entry數組
        private Entry[] table;
    }
}

根據上面的源碼,可以大致畫出ThreadLocal在虛擬機內存中的結構
ThreadLocal內存結構
實線箭頭表示強引用,虛線箭頭表示弱引用(關於對象的四種引用,可以參考我的博客:Java中四種引用)。
需要注意的是:

  • ThreadLocalMap雖然是在ThreadLocal類中定義的,但是實際上被Thread持有。
  • Entry的key是(虛引用的)ThreadLocal對象,而不是當前線程ID或者線程名稱。
  • ThreadLocalMap中持有的是Entry數組,而不是Entry。

對於第一點,ThreadLocalMap被Thread持有是爲了實現每個線程都有自己獨立的ThreadLocalMap對象,以此爲基礎,做到線程隔離。第二點和第三點理解,我們先來想一個問題,如果程序定義了多個ThreadLocal對象,內存結構應該是怎樣的?
此時再來看一下ThreadLocal.set(T)方法:

 public void set(T value) {
 	 // 獲取當前線程對象
     Thread t = Thread.currentThread();
     // 根據線程對象獲取ThreadLocalMap對象(ThreadLocalMap被Thread持有)
     ThreadLocalMap map = getMap(t);
     // 如果ThreadLocalMap存在,則直接插入;不存在,則新建ThreadLocalMap
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value);
 }

也就是說,如果程序定義了多個ThreadLocalMap,會共用一個ThreadLocalMap對象,所以內存結構應該是這樣
ThreadLocal內存結構
這個內存結構圖解釋了第二點和第三點。假設Entry中key爲當前線程ID或者名稱的話,那麼程序中定義多個ThreadLocal對象時,Entry數組中的所有Entry的key都一樣(或者說只能存一個value)。ThreadLocalMap中持有的是Entry數組,而不是Entry,則是因爲程序可定義多個ThreadLocal對象,自然需要一個數組。

內存泄漏

ThreadLocal會發生內存泄漏嗎?
答案是:會。
仔細看下ThreadLocal內存結構就會發現,Entry數組對象通過ThreadLocalMap最終被Thread持有,並且是強引用。也就是說Entry數組對象的生命週期和當前線程一樣。即使ThreadLocal對象被回收了,Entry數組對象也不一定被回收,這樣就有可能發生內存泄漏。ThreadLocal在設計的時候就提供了一些補救措施:

  • Entry的key是弱引用的ThreadLocal對象,很容易被回收,導致key爲null(但是value不爲null)。所以在調用get()set(T)remove()等方法的時候,會自動清理key爲null的Entity。
  • remove()方法就是用來清理無用對象,防止內存泄漏的。所以每次用完ThreadLocal後需要手動remove()

注:有些人認爲是弱引用導致了內存泄漏,其實不對的。假設把弱引用變成強引用,這樣無用的對象key和value都不爲null,反而不利於清理,只能通過remove()方法手動清理,或者等待線程結束生命週期。

應用場景

  • 維護JDBC的java.sql.Connection對象,因爲每個線程都需要保持特定的Connection對象。
  • Web開發時,有些信息需要從controller傳到service傳到dao,甚至傳到util類。看起來非常不優雅,這時便可以使用ThreadLocal來優雅的實現。
  • 包裹線程不安全的工具類,比如Random、SimpleDateFormat等

比較

有些人拿ThreadLocal和synchronized比較,其實他們的指導思想不一樣。

  • synchronized是同一時間最多隻有一個線程執行,所以變量只需要存一份,算是一種時間換空間的思想
  • ThreadLocal是多個線程互不影響,所以每個線程存一份變量,算是一種空間換時間的思想

總結

ThreadLocal是一種隔離的思想,當一個變量需要進行線程隔離時,就可以考慮使用ThreadLocal來優雅的實現。

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