ThreadLocal 到底是什么?我們來一探究竟

一、前言


對一個事務的認知是一個遞進的過程。在了解ThreadLocal時,需要注意以下幾點:

  • 什么是ThreadLocal? ThreadLocal出現的背景是什么?解決了什么問題?
  • ThreadLocal的使用方法是什么?使用的效果如何?
  • ThreadLocal是如何實現它的功能的,即ThreadLocal的原理是什么?

二、背景


??????在一個分布式系統中,多個線程同時訪問同一類實例中的某個變量a,由于變量a是線程共享的,導致一個線程對變量a進行修改,其他線程讀到的都是修改后的變量a的值(如果是存在多個線程同時寫,需要加分布式鎖,限制同時只能一個線程對其進行修改)。這種情況在普通的場景下是合理的,比如在電商系統中,買家點擊支付訂單兩次(兩個獨立的線程),第一次生成訂單,會修改冪等值(防止第二次重復下單),第二次訪問的時候去判斷冪等值,如果已被修改,則不會重新生成訂單。所以線程間變量共享是必須的。
??????但存在這樣一個場景:還是以電商系統為例。買家在訪問訂單詳情頁的時候,在不同的條件下會查訂單(查數據庫)。查庫涉及io,對系統的開銷和響應時間有較大的影響。由于訂單詳情頁的渲染都是一些讀操作,沒有寫操作,所以,需要在查數據庫時做一層本地緩存。而且這個本地緩存是對線程敏感的,只在當前線程生效,別的線程無法訪問這個緩存,也就是說線程間是隔離的。
??????上面只是以電商為例, 所以需要一種方式,能夠實現變量的線程間隔離,此變量只能在當前線程生效,不同的線程變量有不同的值。基于以上訴求,java誕生了ThreadLocal,主要是為了解決內存的線程隔離。

三、使用方式


3.1 測試代碼

  • 線程類
public class NormalThread implements Runnable {

    private int shareValue = 0;
    ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

    @Override
    public void run() {

        shareValue += 1;
        threadLocalValue.set(shareValue);
        System.out.println(Thread.currentThread().getName() + "===== shareValue:" + shareValue + "   threadLocal:" + threadLocalValue.get());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "===== shareValue:" + shareValue + "   threadLocal:" + threadLocalValue.get());
    }
}
  • 線程池
public class MutiThreadUtil {

    private static int core_pool_size = 4;
    private static int max_pool_size = 10;
    //如果空閑立即退出
    private static long keep_alive_time = 0L;
    //隊列的容量是0
    private static BlockingQueue queue = new SynchronousQueue();
    //隊列容量為1
    private static ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue(1);

    public static ExecutorService initThreadPool() {
        ExecutorService executorService = new ThreadPoolExecutor(
            core_pool_size, max_pool_size, keep_alive_time,TimeUnit.SECONDS,queue
        );
        return executorService;
    }
}

  • 主線程
public class ThreadLocalTest {

    public static void main(String[] args) {

        NormalThread normalThread = new NormalThread();
        ExecutorService executorService = MutiThreadUtil.initThreadPool();
        for (int i = 0; i < 10; i ++) {
            executorService.execute(normalThread);
        }
        executorService.shutdown();
    }
}

3.2 結果分析

pool-1-thread-1===== shareValue:1   threadLocal:1
pool-1-thread-2===== shareValue:2   threadLocal:2
pool-1-thread-3===== shareValue:3   threadLocal:3
pool-1-thread-8===== shareValue:4   threadLocal:4
pool-1-thread-4===== shareValue:5   threadLocal:5
pool-1-thread-5===== shareValue:6   threadLocal:6
pool-1-thread-6===== shareValue:7   threadLocal:7
pool-1-thread-7===== shareValue:8   threadLocal:8
pool-1-thread-9===== shareValue:9   threadLocal:9
pool-1-thread-10===== shareValue:10   threadLocal:10
pool-1-thread-3===== shareValue:10   threadLocal:3
pool-1-thread-2===== shareValue:10   threadLocal:2
pool-1-thread-1===== shareValue:10   threadLocal:1
pool-1-thread-8===== shareValue:10   threadLocal:4
pool-1-thread-7===== shareValue:10   threadLocal:8
pool-1-thread-4===== shareValue:10   threadLocal:5
pool-1-thread-5===== shareValue:10   threadLocal:6
pool-1-thread-9===== shareValue:10   threadLocal:9
pool-1-thread-10===== shareValue:10   threadLocal:10
pool-1-thread-6===== shareValue:10   threadLocal:7

以線程pool-1-thread-1線程(后面簡稱線程1)作為分析,剛開始 線程1的shareValue 和 threadlocal 值均為1, shareValue是共享變量,在線程1 sleep階段,線程2-10均執行了以下代碼

 shareValue += 1;
 threadLocalValue.set(shareValue);

但從sleep后的打印結果來看,線程1只是更改了shareValue10的值,變為10, 而threadlocal的值沒有變,還是1,這說明threadlocal的值是線程級的,是線程的私有空間,不會因為其他線程的改變而改變。證明了thread的線程隔離性。

四、ThreadLocal 原理


在不看源碼之前,我們思考下如果讓我們設計這樣一個工具類,能夠使得線程間的變量相互隔離,我們會怎樣設計?
??????每一個線程,其執行均是依靠Thread類的實例的start方法來啟動線程,然后CPU來執行線程。每一個Thread類的實例的運行即為一個線程。若要每個線程(每個Thread實例)的變量空間隔離,則需要將這個變量的定義聲明在Thread這個類中。這樣,每個實例都有屬于自己的這個變量的空間,則實現了線程的隔離。事實上,ThreadLocal的源碼也是這樣實現的。

4.1 實現內存線程間隔離的原理

  1. 在Thread類中聲明一個公共的類變量ThreadLocalMap,用以在Thread的實例中預占空間
ThreadLocal.ThreadLocalMap threadLocals = null;
  1. 在ThreadLocal中創建一個內部類ThreadLocalMap,這個Map的key是ThreadLoca對象,value是set進去的ThreadLocal中泛型類型的值
private void set(ThreadLocal<?> key, Object value) {...}
  1. 在new ThreadLocal時,只是簡單的創建了個ThreadLocal對象,與線程還沒有任何關系
  2. 真正產生關系的是在向ThreadLocal對象中set值得時候
  • 首先從當前的線程中獲取ThreadLocalMap,如果為空,則初始化當前線程的ThreadLocalMap
  • 然后將值set到這個Map中去,如果不為空,則說明當前線程之前已經set過ThreadLocal對象了。
    這樣用一個ThreadHashMap來存儲當前線程的若干個可以線程間隔離的變量,key是ThreadLocal對象,value是要存儲的值(類型是ThreadLocal的泛型)
 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. 從ThreadLocal中獲取值 :還是先從當前線程中獲取ThreadLocalMap,然后使用ThreadLocal對象(key)去獲取這個對象對應的值(value)
 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();
    }

到這里,如果僅僅是理解ThreadLocal是如何實現的線程級別的隔離已經完全足夠了。簡單的講,就是在Thread的類中聲明了ThreadLocalMap這個類,然后在使用ThreadLocal對象set值的時候將當前線程(Thread實例)進行map初始化,并將Threadlocal對應的值塞進map中,下次get的時候,也是使用這個ThreadLcoal的對象(key)去從當前線程的map中獲取值(value)就可以了

4.2 ThreadLocalMap的深究

從源碼上看,ThreadLocalMap雖然叫做Map,但和我們常規理解的Map不太一樣,因為這個類并沒有實現Map這個接口,只是定義在ThreadLocal中的一個靜態內部類。只是因為在存儲的時候也是以key-value的形式作為方法的入參暴露出去,所以稱為map。

static class ThreadLocalMap {...}
  1. ThreadLocalMap的創建,在使用ThreadLocal對象set值的時候,會創建ThreadLocalMap的對象,可以看到,入參就是KV,key是ThreadLocal對象,value是一個Entry對象,存儲kv(HashMap是使用Node作為KV對象存儲)。Entry的key是ThreadLocal對象,vaule是set進去的具體值。
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  1. 繼續看看在創建ThreadLocalMap實例的時候做了什么?其實ThreadLocalMap存儲是一個Entry類型的數組,key提供了hashcode用來計算存儲的數組地址(散列法解決沖突)
  • 創建Entry數組(初始容量16)
  • 然后獲取到key(ThreadLocal對象)的hashcode(是一個自增的原子int型)
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
private static AtomicInteger nextHashCode =    new AtomicInteger();
  • 使用【hashcode 模(%) 數組長度】的方式得到要將key存儲到數組的哪一位。
  • 設置數組的擴容閾值,用以后續擴容
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

創建ThreadLcoalMap對象只有在當前線程第一次插入kv的時候發生,如果是第二次插入kv,則會進行第三步

  1. 這個set的過程其實就是根據ThreadLocal的hashcode來計算存儲在Entry數組的位置
  • 利用ThreadLocal的【hashcode 模(%) 數組長度】的方式獲取存儲在數組的位置
  • 如果當前位置已存在值,則向右移一位,如果也存在值,則繼續右移,直到有空位置出現為止
  • 將當前的value存儲上面兩部得到的索引位置(上面這兩步就是散列法的實現)
  • 校驗是否擴容,如果當前數組的中存儲的值得數量大于閾值(數組長度的2/3),則擴容一倍,并將原來的數組的值重新hash至新數組中(這個過程其實就是HashMap的擴容過程)
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();
        }

4.3 ThreadLocalMap和HashMap的比較

  1. 上述的整個過程其實和HashMap的實現方式很相像,相同點:
  • 兩個map都是最終用數組作為存儲結構,使用key做索引,value是真正存儲在數組索引上的值。
  1. 不同點:解決key沖突的方式
  • map解決沖突的方式不一樣,HashMap采用鏈表法,ThreadLocalMap采用散列法(又稱開放地址法)

思考:為什么不采用HashMap作為ThreadLocal的存儲結構?
個人理解:

  • 引入鏈表,徒增了數據結構的復雜度,并且鏈表的讀取效率較低
  • 更加靈活。包括方法的定義和數組的管理,更加適合當前場景
  • 不需要HashMap的額外的很多方法和變量,需要一個更加純粹和干凈map,來存儲自己需要的值,減少內存的損耗。

4.4 ThreadLocal的生命周期

ThreadLocal的生命周期和當前Thread的生命周期強綁定

  1. 正常情況
    正常情況下(當然會有非正常情況),在線程退出的時候會將threadLocals這個變量置為null,等待JVM去自動回收。

注意:Thread這個方法只是用以系統能夠顯示的調用退出線程,線程在結束的時候是不會調用這個方法,啟動的線程是非守護線程,會在線程結束的時候由jvm自動進行空間的釋放和回收。

private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }
  1. 非正常情況

由于現在多線程一般都是由線程池管理,而線程池的線程一般都是復用的,這樣會導致線程一直存活,而如果使用ThreadLocal大量存儲變量,會使得空間開始膨脹

  1. 啟發
    需要自己來管理ThreadLocal的生命周期,在ThreadLocal使用結束以后及時調用remove()方法進行清理。

五、注意事項


  • 注意管理ThreadLocal的聲明周期,及時調用remove方法進行空間釋放
  • 注意ThreadLocal的使用方式,如果在使用中發現沒有獲取到預期的值,只能是自己的使用方式不對,導致獲取的不是同一線程下的ThreadLocal值。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,156評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,866評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 174,880評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,398評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,202評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,743評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,822評論 3 438
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 41,962評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,476評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,444評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,579評論 1 365
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,129評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,840評論 3 344
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,231評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,487評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,177評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,568評論 2 370

推薦閱讀更多精彩內容

  • 01/ 十一月份的北方已經進入了大冬季,屋內暖氣送的極其的旺,屋外寒風蕭瑟,落葉散落了滿地,北方的冬季就是這樣蕭瑟...
    治愈里閱讀 509評論 1 2
  • 協會換屆在即,作為15級的老學姐,有一番話想說給16級的各位部長們。 親愛小石榴們: 你們好。 轉眼,我們相處已經...
    理智瘋子閱讀 256評論 0 2
  • 前幾天我的一個朋友和他男朋友鬧分手,我這個朋友跑過來問我,“如果你的男朋友對所有女生都一樣好,都一樣貼心,然后他們...
    冰月貳玖閱讀 278評論 0 1