国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務(wù)器之家:專注于服務(wù)器技術(shù)及軟件下載分享
分類導(dǎo)航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術(shù)|正則表達(dá)式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務(wù)器之家 - 編程語言 - Java教程 - 并發(fā)編程之ThreadLocal深入理解

并發(fā)編程之ThreadLocal深入理解

2020-12-11 23:46一角錢技術(shù) Java教程

在日常的開發(fā)中,我們經(jīng)常會(huì)遇到在當(dāng)前運(yùn)行線程中保存一些信息,并且各線程之間是隔離的,不會(huì)相互影響,不存在并發(fā)問題,通過這樣的方式來實(shí)現(xiàn)請(qǐng)求調(diào)用鏈中方法之間參數(shù)傳遞的解耦,提升代碼結(jié)構(gòu)的穩(wěn)定性等。Java Threa

 前言

在日常的開發(fā)中,我們經(jīng)常會(huì)遇到在當(dāng)前運(yùn)行線程中保存一些信息,并且各線程之間是隔離的,不會(huì)相互影響,不存在并發(fā)問題,通過這樣的方式來實(shí)現(xiàn)請(qǐng)求調(diào)用鏈中方法之間參數(shù)傳遞的解耦,提升代碼結(jié)構(gòu)的穩(wěn)定性等。Java ThreadLocal就是用于實(shí)現(xiàn)這一目標(biāo)的。在學(xué)習(xí)之前我們先帶著以下幾個(gè)問題:

  1. ThreadLocal 是什么?
  2. ThreadLocal 怎么用?
  3. ThreadLocal 和線程同步機(jī)制相比較?
  4. ThreadLocal 是如何實(shí)現(xiàn)線程隔離的呢?
  5. ThreadLocal 如何避免內(nèi)存泄漏呢?
  6. ThreadLocal 與 Thread、ThreadLocalMap 之間的關(guān)系?

以下分析均基于JDK1.8。

什么是ThreadLocal

ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲(chǔ)。

ThreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,那么每個(gè)線程可以訪問自己內(nèi)部的副本變量,這樣同時(shí)多個(gè)線程訪問該變量并不會(huì)彼此相互影響,因此他們使用的都是自己從內(nèi)存中拷貝過來的變量的副本,這樣就不存在線程安全問題,也不會(huì)影響程序的執(zhí)行性能。

注意:雖然ThreadLocal能夠解決上面說的問題,但是由于在每個(gè)線程中都創(chuàng)建了副本,所以要考慮它對(duì)資源的消耗,比如內(nèi)存的占用會(huì)比不使用ThreadLocal要大。

ThreadLocal 怎么用

通常使用靜態(tài)的變量來維護(hù)ThreadLocal,如:

  1. static ThreadLocal<String> sThreadLocal = new ThreadLocal<String> 

會(huì)自動(dòng)在每一個(gè)線程上創(chuàng)建一個(gè) T 的副本,副本之間彼此獨(dú)立,互不影響,可以用 ThreadLocal 存儲(chǔ)一些參數(shù),以便在線程中多個(gè)方法中使用,用以代替方法傳參的做法。

通過一個(gè)例子來了解 ThreadLocal:

  1. package com.niuh.threadlocal; 
  2.  
  3. /** 
  4.  * <p> 
  5.  * ThreadLocal 示例 
  6.  * </p> 
  7.  */ 
  8. public class ThreadLocalDemo { 
  9.     /** 
  10.      * ThreadLocal變量,每個(gè)線程都有一個(gè)副本,互不干擾 
  11.      */ 
  12.     public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>(); 
  13.  
  14.     public static void main(String[] args) throws Exception { 
  15.         new ThreadLocalDemo().threadLocalTest(); 
  16.     } 
  17.  
  18.     public void threadLocalTest() throws Exception { 
  19.         // 主線程設(shè)置值 
  20.         THREAD_LOCAL.set("一角錢技術(shù)"); 
  21.         String v = THREAD_LOCAL.get(); 
  22.         System.out.println("Thread-0線程執(zhí)行之前," + Thread.currentThread().getName() + "線程取到的值:" + v); 
  23.  
  24.         new Thread(new Runnable() { 
  25.             @Override 
  26.             public void run() { 
  27.                 String v = THREAD_LOCAL.get(); 
  28.                 System.out.println(Thread.currentThread().getName() + "線程取到的值:" + v); 
  29.                 // 設(shè)置 threadLocal 
  30.                 THREAD_LOCAL.set("一角錢技術(shù)2020"); 
  31.                 v = THREAD_LOCAL.get(); 
  32.                 System.out.println("重新設(shè)置之后," + Thread.currentThread().getName() + "線程取到的值為:" + v); 
  33.                 System.out.println(Thread.currentThread().getName() + "線程執(zhí)行結(jié)束"); 
  34.             } 
  35.         }).start(); 
  36.         // 等待所有線程執(zhí)行結(jié)束 
  37.         Thread.sleep(3000L); 
  38.         v = THREAD_LOCAL.get(); 
  39.         System.out.println("Thread-0線程執(zhí)行之后," + Thread.currentThread().getName() + "線程取到的值:" + v); 
  40.     } 

首先通過 static final 定義了一個(gè) THREAD_LOCAL 變量,其中 static 是為了確保全局只有一個(gè)保存 String 對(duì)象的 ThreadLocal 實(shí)例;final 確保 ThreadLocal 的實(shí)例不可更改,防止被意外改變,導(dǎo)致放入的值和取出來的不一致,另外還能防止 ThreadLocal 的內(nèi)存泄漏。上面的例子是演示在不同的線程中獲取它會(huì)得到不同的結(jié)果,運(yùn)行結(jié)果如下:

  1. Thread-0線程執(zhí)行之前,main線程取到的值:一角錢技術(shù) 
  2. Thread-0線程取到的值:null 
  3. 重新設(shè)置之后,Thread-0線程取到的值為:一角錢技術(shù)2020 
  4. Thread-0線程執(zhí)行結(jié)束 
  5. Thread-0線程執(zhí)行之后,main線程取到的值:一角錢技術(shù) 

  • 首先在 Thread-0 線程執(zhí)行之前,先給 THREAD_LOCAL 設(shè)置為 一角錢技術(shù),然后可以取到這個(gè)值;
  • 然后通過創(chuàng)建一個(gè)新的線程以后去取這個(gè)值,發(fā)現(xiàn)新線程取到的為 null,意味著這個(gè)變量在不同線程中取到的值是不同的,不同線程之間對(duì)于 ThreadLocal 會(huì)有對(duì)應(yīng)的副本;
  • 接著在線程 Thread-0 中執(zhí)行對(duì) THREAD_LOCAL 的修改,將值改為 一角錢技術(shù)2020,可以發(fā)現(xiàn)線程 Thread-0 獲取的值變?yōu)榱?一角錢技術(shù)2020,主線程依然會(huì)讀取到屬于它的副本數(shù)據(jù) 一角錢技術(shù),這就是線程的封閉。

看到這里,我相信大家一定會(huì)好奇 ThreadLocal 是如何做到多個(gè)線程對(duì)同一對(duì)象 set 操作,但是 get 獲取的值還都是每個(gè)線程 set 的值呢。

ThreadLocal和線程同步機(jī)制相比較

ThreadLocal和線程同步機(jī)制都是為了解決多線程中相同變量的訪問沖突問題。

在同步機(jī)制中,通過對(duì)象的鎖機(jī)制保證同一時(shí)間只有一個(gè)線程訪問變量。這時(shí)該變量是多個(gè)線程共享的,使用同步機(jī)制要求程序慎密地分析什么時(shí)候?qū)ψ兞窟M(jìn)行讀寫,什么時(shí)候需要鎖定某個(gè)對(duì)象,什么時(shí)候釋放對(duì)象鎖等繁雜的問題,程序設(shè)計(jì)和編寫難度相對(duì)較大。

而ThreadLocal則從另一個(gè)角度來解決多線程的并發(fā)訪問。ThreadLocal會(huì)為每一個(gè)線程提供一個(gè)獨(dú)立的變量副本,從而隔離了多個(gè)線程對(duì)數(shù)據(jù)的訪問沖突。因?yàn)槊恳粋€(gè)線程都擁有自己的變量副本,從而也就沒有必要對(duì)該變量進(jìn)行同步了。ThreadLocal提供了線程安全的共享對(duì)象,在編寫多線程代碼時(shí),可以把不安全的變量封裝進(jìn)ThreadLocal。

總的來說,對(duì)于多線程資源共享的問題,同步機(jī)制采用了“以時(shí)間換空間”的方式,而ThreadLocal采用了“以空間換時(shí)間”的方式。前者僅提供一份變量,讓不同的線程排隊(duì)訪問,而后者為每一個(gè)線程都提供了一份變量,因此可以同時(shí)訪問而互不影響。

ThreadLocal源碼解析

并發(fā)編程之ThreadLocal深入理解

成員變量

  1. // 當(dāng)前 ThreadLocal 的 hashCode,由 nextHashCode() 計(jì)算而來 
  2. // 用于計(jì)算當(dāng)前 ThreadLocal 在 ThreadLocalMap 中的索引位置 
  3. private final int threadLocalHashCode = nextHashCode(); 
  4. // 哈希魔數(shù),主要與斐波那契散列法以及黃金分割有關(guān) 
  5. private static final int HASH_INCREMENT = 0x61c88647; 
  6. // 返回計(jì)算出的下一個(gè)哈希值,其值為 i * HASH_INCREMENT,其中 i 代表調(diào)用次數(shù) 
  7. private static int nextHashCode() { 
  8.     return nextHashCode.getAndAdd(HASH_INCREMENT); 
  9. // 保證了在一臺(tái)機(jī)器中每個(gè) ThreadLocal 的 threadLocalHashCode 是唯一的 
  10. private static AtomicInteger nextHashCode = new AtomicInteger(); 

其中的 HASH_INCREMENT 也不是隨便取的,它轉(zhuǎn)化為十進(jìn)制是 1640531527,2654435769 轉(zhuǎn)換成 int 類型就是 -1640531527,2654435769 等于 (√5-1)/2 乘以 2 的 32 次方。(√5-1)/2 就是黃金分割數(shù),近似為 0.618,也就是說 0x61c88647 理解為一個(gè)黃金分割數(shù)乘以 2 的 32 次方,它可以保證 nextHashCode 生成的哈希值,均勻的分布在 2 的冪次方上,且小于 2 的 32 次方。

下面是 javaspecialists 中一篇文章對(duì)它的介紹:

  • This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.

下面用例子來證明下:

  1. private static final int HASH_INCREMENT = 0x61c88647; 
  2.  
  3. public static void main(String[] args) throws Exception { 
  4.     int n = 5; 
  5.     int max = 2 << (n - 1); 
  6.     for (int i = 0; i < max; i++) { 
  7.         System.out.print(i * HASH_INCREMENT & (max - 1)); 
  8.         System.out.print(" "); 
  9.  
  10.     } 

運(yùn)行結(jié)果為:0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25

可以發(fā)現(xiàn)元素索引值完美的散列在數(shù)組當(dāng)中,并沒有出現(xiàn)沖突。

內(nèi)部類ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的靜態(tài)內(nèi)部類,當(dāng)一個(gè)線程有多個(gè) ThreadLocal 時(shí),需要一個(gè)容器來管理多個(gè) ThreadLocal,ThreadLocalMap 的作用就是管理線程中多個(gè) ThreadLocal。

ThreadLocalMap 其實(shí)就是一個(gè)簡(jiǎn)單的 Map 結(jié)構(gòu),底層是數(shù)組,有初始化大小,也有擴(kuò)容閾值大小,數(shù)組的元素是 Entry。

ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)是一個(gè)用數(shù)組表示的環(huán),數(shù)組長(zhǎng)度必須是2的次冪,同樣通過hash方式確定節(jié)點(diǎn)在數(shù)組中的下標(biāo)(hash值是ThreadLocal的遞增變量,而不是hashcode值),對(duì)于hash沖突的情況,采用線性探測(cè)法,直接將元素防止對(duì)應(yīng)下標(biāo)后面的下一個(gè)空閑單元。

ThreadLocalMap的key采用的是弱引用WeakReference,因此在使用過程中還需要注意及時(shí)清理key已經(jīng)被gc回收的節(jié)點(diǎn),及時(shí)釋放無效空間。

關(guān)于弱引用可以查看《Java基礎(chǔ) |強(qiáng)引用、弱引用、軟引用、虛引用》

成員屬性

  1. // 初始容量,必須為 2 的冪 
  2. private static final int INITIAL_CAPACITY = 16; 
  3.  
  4. // 存儲(chǔ) ThreadLocal 的鍵值對(duì)實(shí)體數(shù)組,長(zhǎng)度必須為 2 的冪 
  5. private Entry[] table
  6.  
  7. // ThreadLocalMap 元素?cái)?shù)量 
  8. private int size = 0; 
  9.  
  10. //擴(kuò)容的閾值,默認(rèn)是數(shù)組大小的三分之二 
  11. private int threshold; // Default to 0 

Entry類

Entry是ThreadLocalMap的內(nèi)部類,用來表示其中的節(jié)點(diǎn),繼承了弱引用WeadReference類。

  1. // 鍵值對(duì)實(shí)體的存儲(chǔ)結(jié)構(gòu) 
  2. static class Entry extends WeakReference<ThreadLocal<?>> { 
  3.     // 當(dāng)前線程關(guān)聯(lián)的 value,這個(gè) value 并沒有用弱引用追蹤 
  4.     Object value; 
  5.     /** 
  6.   * 構(gòu)造鍵值對(duì) 
  7.      * 
  8.   * @param k k 作 key,作為 key 的 ThreadLocal 會(huì)被包裝為一個(gè)弱引用 
  9.   * @param v v 作 value 
  10.   */   
  11.     Entry(ThreadLocal<?> k, Object v) { 
  12.         super(k); 
  13.         value = v; 
  14.     } 

Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。同時(shí),Entry也繼承WeakReference,所以說Entry所對(duì)應(yīng)key(ThreadLocal實(shí)例)的引用是一個(gè)弱引用。

  • 弱引用的對(duì)象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。不過,由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線程,因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對(duì)象。

構(gòu)造方法

1.ThreadLocalMap 提供了兩個(gè)構(gòu)造方法:

  1. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 
  2.     table = new Entry[INITIAL_CAPACITY]; 
  3.     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 
  4.     table[i] = new Entry(firstKey, firstValue); 
  5.     size = 1; 
  6.     setThreshold(INITIAL_CAPACITY); 

  • 根據(jù)第一個(gè)節(jié)點(diǎn)的key和value初始化map。
  • 初始化數(shù)組,確定節(jié)點(diǎn)在數(shù)組的下標(biāo),初始化table[i],設(shè)置size和threshold。
  • 進(jìn)行散列的hash值是ThreadLocal的threadLocalHashCode,遞增生成。

2.ThreadLocalMap#ThreadLocalMap(ThreadLocalMap)

  1. private ThreadLocalMap(ThreadLocalMap parentMap) { 
  2.     Entry[] parentTable = parentMap.table
  3.     int len = parentTable.length; 
  4.     setThreshold(len); 
  5.     table = new Entry[len]; 
  6.  
  7.     for (int j = 0; j < len; j++) { 
  8.         Entry e = parentTable[j]; 
  9.         if (e != null) { 
  10.             @SuppressWarnings("unchecked"
  11.             ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); 
  12.             if (key != null) { 
  13.                 Object value = key.childValue(e.value); 
  14.                 Entry c = new Entry(key, value); 
  15.                 int h = key.threadLocalHashCode & (len - 1); 
  16.                 while (table[h] != null
  17.                     h = nextIndex(h, len); 
  18.                 table[h] = c; 
  19.                 size++; 
  20.             } 
  21.         } 
  22.     } 

初始化數(shù)組和threshold,遍歷節(jié)點(diǎn)加入數(shù)組。

擦除機(jī)制

ThreadLocalMap中內(nèi)部類Entry,繼承了WeakReference,其key值是弱引用類型,在沒有強(qiáng)引用時(shí)會(huì)被gc回收,因此ThreadLocalMap要及時(shí)對(duì)這部分過期節(jié)點(diǎn)進(jìn)行擦除。

1.ThreadLocalMap#expungeStaleEntry(int)

  1. private int expungeStaleEntry(int staleSlot) { 
  2.     Entry[] tab = table
  3.     int len = tab.length; 
  4.  
  5.     // expunge entry at staleSlot 
  6.     tab[staleSlot].value = null
  7.     tab[staleSlot] = null
  8.     size--; 
  9.  
  10.     // Rehash until we encounter null 
  11.     Entry e; 
  12.     int i; 
  13.     for (i = nextIndex(staleSlot, len); 
  14.          (e = tab[i]) != null
  15.          i = nextIndex(i, len)) { 
  16.         ThreadLocal<?> k = e.get(); 
  17.         if (k == null) { 
  18.             e.value = null
  19.             tab[i] = null
  20.             size--; 
  21.         } else { 
  22.             int h = k.threadLocalHashCode & (len - 1); 
  23.             if (h != i) { 
  24.                 tab[i] = null
  25.  
  26.                 // Unlike Knuth 6.4 Algorithm R, we must scan until 
  27.                 // null because multiple entries could have been stale. 
  28.                 while (tab[h] != null
  29.                     h = nextIndex(h, len); 
  30.                 tab[h] = e; 
  31.             } 
  32.         } 
  33.     } 
  34.     return i; 

擦除staleSlot處的無效節(jié)點(diǎn),同時(shí)掃描處于staleSlot + 1 – 下一個(gè)null節(jié)點(diǎn)之間的節(jié)點(diǎn),對(duì)于過期節(jié)點(diǎn)進(jìn)行擦除,有效節(jié)點(diǎn)rehash,判斷是否需要修改位置。

2.ThreadLocalMap#expungeStaleEntries()

  1. private void expungeStaleEntries() { 
  2.     Entry[] tab = table
  3.     int len = tab.length; 
  4.     for (int j = 0; j < len; j++) { 
  5.         Entry e = tab[j]; 
  6.         if (e != null && e.get() == null
  7.             expungeStaleEntry(j); 
  8.     } 

全量掃描擦除,遍歷數(shù)組中的所有節(jié)點(diǎn),對(duì)于過期節(jié)點(diǎn)調(diào)用擦除方法expungeStaleEntry進(jìn)行擦除。

3.ThreadLocalMap#cleanSomeSlots(int i, int n)

  1. private boolean cleanSomeSlots(int i, int n) { 
  2.     boolean removed = false
  3.     Entry[] tab = table
  4.     int len = tab.length; 
  5.     do { 
  6.         i = nextIndex(i, len); 
  7.         Entry e = tab[i]; 
  8.         if (e != null && e.get() == null) { 
  9.             n = len; 
  10.             removed = true
  11.             i = expungeStaleEntry(i); 
  12.         } 
  13.     } while ( (n >>>= 1) != 0); 
  14.     return removed; 

啟發(fā)式掃描擦除。從 i+1 開始掃描檢查,如果連續(xù)log n個(gè)單元不需要擦除則結(jié)束方法,否則找到一個(gè)過期節(jié)點(diǎn),重置計(jì)數(shù),將n置為數(shù)組長(zhǎng)度,重新開始新一輪的掃描。只有掃描過程中有一個(gè)過期節(jié)點(diǎn),則認(rèn)為擦除成功,返回true。

ThreadLocalMap#getEntry(ThreadLocal)

  1. /** 
  2.  * 返回 key 關(guān)聯(lián)的鍵值對(duì)實(shí)體 
  3.  * 
  4.  * @param key threadLocal 
  5.  * @return 
  6.  */ 
  7. private Entry getEntry(ThreadLocal<?> key) { 
  8.     int i = key.threadLocalHashCode & (table.length - 1); 
  9.     Entry e = table[i]; 
  10.     // 若 e 不為空,并且 e 的 ThreadLocal 的內(nèi)存地址和 key 相同,直接返回 
  11.     if (e != null && e.get() == key) { 
  12.         return e; 
  13.     } else { 
  14.         // 碰撞查找,從 i 開始向后遍歷找到鍵值對(duì)實(shí)體 
  15.         return getEntryAfterMiss(key, i, e); 
  16.     } 

我們?cè)賮砜匆幌耮etEntryAfterMiss方法:

  1. private Entry getEntryAfterMiss(ThreadLocal<?> keyint i, Entry e) { 
  2.     Entry[] tab = table
  3.     int len = tab.length; 
  4.  
  5.     while (e != null) { 
  6.         ThreadLocal<?> k = e.get(); 
  7.         if (k == key
  8.             return e; 
  9.         if (k == null
  10.             expungeStaleEntry(i); 
  11.         else 
  12.             i = nextIndex(i, len); 
  13.         e = tab[i]; 
  14.     } 
  15.     return null

用于在查找節(jié)點(diǎn)時(shí)沒有直接命中的情況下進(jìn)行線性的碰撞查找,對(duì)照查找過程中的過期節(jié)點(diǎn),進(jìn)行擦除。

ThreadLocalMap#remove(ThreadLocal)

  1. private void remove(ThreadLocal<?> key) { 
  2.     Entry[] tab = table
  3.     int len = tab.length; 
  4.     int i = key.threadLocalHashCode & (len-1); 
  5.     for (Entry e = tab[i]; 
  6.          e != null
  7.          e = tab[i = nextIndex(i, len)]) { 
  8.         if (e.get() == key) { 
  9.             e.clear(); 
  10.             expungeStaleEntry(i); 
  11.             return
  12.         } 
  13.     } 

根據(jù)key值移除節(jié)點(diǎn)。找到節(jié)點(diǎn)后不是簡(jiǎn)單的將該節(jié)點(diǎn)置為null,還需要調(diào)用擦除方法,不然該節(jié)點(diǎn)后面的hash沖突節(jié)點(diǎn)會(huì)無法通過getEntry獲取到。

ThreadLocalMap#set(ThreadLocal, Object)

調(diào)用set() 時(shí),會(huì)把當(dāng)前 threadLocal 對(duì)象作為 key,想要保存的對(duì)象作為 value,存入 map。用于增加或覆蓋節(jié)點(diǎn),類似于Map接口的put方法。

  1. /** 
  2.  * 在 map 中存儲(chǔ)鍵值對(duì)<key, value> 
  3.  * 
  4.  * @param key   threadLocal 
  5.  * @param value 要設(shè)置的 value 值 
  6.  */ 
  7. private void set(ThreadLocal<?> key, Object value) { 
  8.  Entry[] tab = table
  9.  int len = tab.length; 
  10.  // 計(jì)算 key 在數(shù)組中的下標(biāo) 
  11.  int i = key.threadLocalHashCode & (len - 1); 
  12.  // 遍歷一段連續(xù)的元素,以查找匹配的 ThreadLocal 對(duì)象 
  13.  for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { 
  14.   // 獲取該哈希值處的ThreadLocal對(duì)象 
  15.   ThreadLocal<?> k = e.get(); 
  16.  
  17.   // 鍵值ThreadLocal匹配,直接更改map中的value 
  18.   if (k == key) { 
  19.    e.value = value; 
  20.    return
  21.   } 
  22.  
  23.   // 若 key 是 null,說明 ThreadLocal 被清理了,直接替換掉 
  24.   if (k == null) { 
  25.    replaceStaleEntry(key, value, i); 
  26.    return
  27.   } 
  28.  } 
  29.  
  30.  // 直到遇見了空槽也沒找到匹配的ThreadLocal對(duì)象,那么在此空槽處安排ThreadLocal對(duì)象和緩存的value 
  31.  tab[i] = new Entry(key, value); 
  32.  int sz = ++size
  33.  // 進(jìn)行啟發(fā)式擦除,節(jié)點(diǎn)數(shù)量大于閾值。如果右節(jié)點(diǎn)擦除成功,節(jié)點(diǎn)數(shù)量不可能大于閾值 
  34.  if (!cleanSomeSlots(i, sz) && sz >= threshold) { 
  35.   // 擴(kuò)容的過程也是對(duì)所有的 key 重新哈希的過程 
  36.   rehash(); 
  37.  } 

我們依次來看看調(diào)用的幾個(gè)方法:

1.ThreadLocalMap#replaceStaleEntry(ThreadLocal, Object, int)

  1. private void replaceStaleEntry(ThreadLocal<?> key, Object value, 
  2.                                 int staleSlot) { 
  3.      Entry[] tab = table
  4.      int len = tab.length; 
  5.      Entry e; 
  6.  
  7.      // Back up to check for prior stale entry in current run. 
  8.      // We clean out whole runs at a time to avoid continual 
  9.      // incremental rehashing due to garbage collector freeing 
  10.      // up refs in bunches (i.e., whenever the collector runs). 
  11.      int slotToExpunge = staleSlot; 
  12.      for (int i = prevIndex(staleSlot, len); 
  13.           (e = tab[i]) != null
  14.           i = prevIndex(i, len)) 
  15.          if (e.get() == null
  16.              slotToExpunge = i; 
  17.  
  18.      // Find either the key or trailing null slot of run, whichever 
  19.      // occurs first 
  20.      for (int i = nextIndex(staleSlot, len); 
  21.           (e = tab[i]) != null
  22.           i = nextIndex(i, len)) { 
  23.          ThreadLocal<?> k = e.get(); 
  24.  
  25.          // If we find keythen we need to swap it 
  26.          // with the stale entry to maintain hash table order
  27.          // The newly stale slot, or any other stale slot 
  28.          // encountered above it, can then be sent to expungeStaleEntry 
  29.          // to remove or rehash all of the other entries in run. 
  30.          if (k == key) { 
  31.              e.value = value; 
  32.  
  33.              tab[i] = tab[staleSlot]; 
  34.              tab[staleSlot] = e; 
  35.  
  36.              // Start expunge at preceding stale entry if it exists 
  37.              if (slotToExpunge == staleSlot) 
  38.                  slotToExpunge = i; 
  39.              cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); 
  40.              return
  41.          } 
  42.  
  43.          // If we didn't find stale entry on backward scan, the 
  44.          // first stale entry seen while scanning for key is the 
  45.          // first still present in the run. 
  46.          if (k == null && slotToExpunge == staleSlot) 
  47.              slotToExpunge = i; 
  48.      } 
  49.  
  50.      // If key not found, put new entry in stale slot 
  51.      tab[staleSlot].value = null
  52.      tab[staleSlot] = new Entry(key, value); 
  53.  
  54.      // If there are any other stale entries in run, expunge them 
  55.      if (slotToExpunge != staleSlot) 
  56.          cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); 
  57.  } 

slotToExpunge 表示第一個(gè)過期節(jié)點(diǎn)

  • 從staleSlot向前掃描,掃描到第一個(gè)為null的節(jié)點(diǎn)截止,如果中間有過期節(jié)點(diǎn),記錄掃描過程中遇到的最后一個(gè)過期節(jié)點(diǎn)的下標(biāo)為 slotToExpunge;
  • 從staleSlot向后掃描,掃描找到key值對(duì)應(yīng)的節(jié)點(diǎn)或null節(jié)點(diǎn)截止:如果在 [從staleSlot向前掃描] 中沒有找到過期節(jié)點(diǎn),需要本次掃描中遇到的第一個(gè)過期節(jié)點(diǎn)的下標(biāo)記錄為 slotToExpunge ;如果找到來 key值對(duì)應(yīng)的節(jié)點(diǎn),覆蓋后將該節(jié)點(diǎn)移到 staleSlot 處,并將該節(jié)點(diǎn)的原來的位置作為過期節(jié)點(diǎn)處理;如果沒有找到節(jié)點(diǎn),新建節(jié)點(diǎn)放置到 staleSlot 處。
  • 如果在兩次掃描中找到了過期節(jié)點(diǎn),先對(duì)該節(jié)點(diǎn)進(jìn)行擦除,并調(diào)用啟發(fā)式掃描擦除。

總體來說,假如 i 下標(biāo)處的節(jié)點(diǎn)是 staleSlot 節(jié)點(diǎn)左邊離得最近的null節(jié)點(diǎn),j 下標(biāo)處的節(jié)點(diǎn)是 staleSlot 節(jié)點(diǎn)右邊離得最近的null節(jié)點(diǎn),并且key值對(duì)應(yīng)的節(jié)點(diǎn)作為過期節(jié)點(diǎn)處理。

那么該方法的功能就兩段:

  • 將 key、value 組成節(jié)點(diǎn)放到 staleSlot 處;
  • 如果在(i — j)的序列中掃描到了過期節(jié)點(diǎn),那么擦除該節(jié)點(diǎn),并從該節(jié)點(diǎn)后的第一個(gè)null節(jié)點(diǎn)開始啟發(fā)式擦除。

之所以需要向前掃描,是為了避免在掃描過程中對(duì)有效節(jié)點(diǎn)的rehash后出現(xiàn)由過期節(jié)點(diǎn)導(dǎo)致的hash沖突。

2.ThreadLocalMap#rehash()

  1. private void rehash() { 
  2.  expungeStaleEntries(); 
  3.  
  4.     // Use lower threshold for doubling to avoid hysteresis 
  5.  if (size >= threshold - threshold / 4) 
  6.   resize(); 

啟動(dòng)全局掃描擦除,擦除后再次判斷是否需要擴(kuò)容。之所以叫做rehash,可以理解成在全局掃描中所有的有效節(jié)點(diǎn)都需要重新hash確定位置。可以看到,并不是節(jié)點(diǎn)數(shù)量大于閾值后就會(huì)觸發(fā)擴(kuò)容,只有全局掃描擦除后數(shù)量仍大于閾值的3/4(容量的1/2)才會(huì)進(jìn)行擴(kuò)容。

3.ThreadLocalMap#resize()

  1. /** 
  2. * 擴(kuò)容,重新計(jì)算索引,標(biāo)記垃圾值,方便 GC 回收 
  3. */ 
  4. private void resize() { 
  5.  Entry[] oldTab = table
  6.  int oldLen = oldTab.length; 
  7.     // 新建一個(gè)數(shù)組,按照2倍長(zhǎng)度擴(kuò)容 
  8.  int newLen = oldLen * 2; 
  9.  Entry[] newTab = new Entry[newLen]; 
  10.  int count = 0; 
  11.  // 將舊數(shù)組的值拷貝到新數(shù)組上 
  12.  for (int j = 0; j < oldLen; ++j) { 
  13.   Entry e = oldTab[j]; 
  14.   if (e != null) { 
  15.    ThreadLocal<?> k = e.get(); 
  16.             // 若有垃圾值,則標(biāo)記清理該元素的引用,以便GC回收 
  17.    if (k == null) { 
  18.     e.value = null; // Help the GC 
  19.    } else { 
  20.                 // 計(jì)算 ThreadLocal 在新數(shù)組中的位置 
  21.     int h = k.threadLocalHashCode & (newLen - 1); 
  22.                 如果發(fā)生沖突,使用線性探測(cè)往后尋找合適的位置 
  23.     while (newTab[h] != null
  24.      h = nextIndex(h, newLen); 
  25.                     newTab[h] = e; 
  26.                     count++; 
  27.                 } 
  28.             } 
  29.  } 
  30.  // 設(shè)置新的擴(kuò)容閥值,為數(shù)組成都的三分之二 
  31.  setThreshold(newLen); 
  32.  size = count
  33.  table = newTab; 

建立新數(shù)組,容量為原來的2倍,遍歷數(shù)組中的元素,將有效節(jié)點(diǎn)hash后放入新數(shù)組,設(shè)置threshold,size等屬性。

ThreadLocal的 remove 方法

remove 方法源碼如下所示:

  1. /** 
  2.  * 清理當(dāng)前 ThreadLocal 對(duì)象關(guān)聯(lián)的鍵值對(duì) 
  3.  */ 
  4. public void remove() { 
  5.  // 返回當(dāng)前線程持有的 map 
  6.  ThreadLocalMap m = getMap(Thread.currentThread()); 
  7.  if (m != null) { 
  8.   // 從 map 中清理當(dāng)前 ThreadLocal 對(duì)象關(guān)聯(lián)的鍵值對(duì) 
  9.   m.remove(this); 
  10.  } 

remove 方法的時(shí)序圖如下所示:

并發(fā)編程之ThreadLocal深入理解

remove 方法是先獲取到當(dāng)前線程的 ThreadLocalMap,并且調(diào)用了它的 remove 方法,從 map 中清理當(dāng)前 ThreadLocal 對(duì)象關(guān)聯(lián)的鍵值對(duì),這樣 value 就可以被 GC 回收了。

ThreadLocal的 set 方法

set 方法源碼如下:

  1. /** 
  2.  * 為當(dāng)前 ThreadLocal 對(duì)象關(guān)聯(lián) value 值 
  3.  * 
  4.  * @param value 要存儲(chǔ)在此線程的線程副本的值 
  5.  */ 
  6. public void set(T value) { 
  7.  // 返回當(dāng)前ThreadLocal所在的線程 
  8.  Thread t = Thread.currentThread(); 
  9.  // 返回當(dāng)前線程持有的map 
  10.  ThreadLocalMap map = getMap(t); 
  11.  if (map != null) { 
  12.   // 如果 ThreadLocalMap 不為空,則直接存儲(chǔ)<ThreadLocal, T>鍵值對(duì) 
  13.   map.set(this, value); 
  14.  } else { 
  15.   // 否則,需要為當(dāng)前線程初始化 ThreadLocalMap,并存儲(chǔ)鍵值對(duì) <this, firstValue> 
  16.   createMap(t, value); 
  17.  } 

set 方法的作用是把我們想要存儲(chǔ)的 value 給保存進(jìn)去。其主要流程為:

  1. 先獲取當(dāng)當(dāng)前線程的引用;
  2. 利用這個(gè)引用來獲取到 ThreadLocalMap;
  3. 如果 map 為空,則去創(chuàng)建一個(gè) ThreadLocalMap;
  4. 如果 map 不為空,就利用 ThreadLocalMap 的 set 方法將 value 添加到 map 中。
  • 其中 map 就是 ThreadLocalMap。

調(diào)用 ThreadLocalMap.set() 時(shí),會(huì)把當(dāng)前 threadLocal 對(duì)象作為 key,想要保存的對(duì)象作為 value,存入 map。

set 方法的時(shí)序圖如下所示:

并發(fā)編程之ThreadLocal深入理解

ThreadLocal的 getMap 方法

  1. /** 
  2.  * 返回當(dāng)前線程 thread 持有的 ThreadLocalMap 
  3.  * 
  4.  * @param t 當(dāng)前線程 
  5.  * @return ThreadLocalMap 
  6.  */ 
  7. ThreadLocalMap getMap(Thread t) { 
  8.  return t.threadLocals; 

getMap 方法的作用主要是獲取當(dāng)前線程內(nèi)的 ThreadLocalMap 對(duì)象,原來這個(gè) ThreadLocalMap 是線程Thread類的一個(gè)屬性,我們來看看 Thread 中相關(guān)的代碼:

  1. /** 
  2.  * ThreadLocal 的 ThreadLocalMap 是線程的一個(gè)屬性,所以在多線程環(huán)境下 threadLocals 是線程安全的 
  3.  */ 
  4. ThreadLocal.ThreadLocalMap threadLocals = null

ThreadLocal的 get 方法

get 方法源碼如下:

  1. /** 
  2.  * 返回當(dāng)前 ThreadLocal 對(duì)象關(guān)聯(lián)的值 
  3.  * 
  4.  * @return 
  5.  */ 
  6. public T get() { 
  7.  // 返回當(dāng)前 ThreadLocal 所在的線程 
  8.  Thread t = Thread.currentThread(); 
  9.  // 從線程中拿到 ThreadLocalMap 
  10.  ThreadLocalMap map = getMap(t); 
  11.  if (map != null) { 
  12.   // 從 map 中拿到 entry 
  13.   ThreadLocalMap.Entry e = map.getEntry(this); 
  14.   // 如果不為空,讀取當(dāng)前 ThreadLocal 中保存的值 
  15.   if (e != null) { 
  16.    @SuppressWarnings("unchecked"
  17.    T result = (T) e.value; 
  18.    return result; 
  19.   } 
  20.  } 
  21.  // 若 map 為空,則對(duì)當(dāng)前線程的 ThreadLocal 進(jìn)行初始化,最后返回當(dāng)前的 ThreadLocal 對(duì)象關(guān)聯(lián)的初值,即 value 
  22.  return setInitialValue(); 

get 方法的主要流程為:

  1. 先獲取到當(dāng)前線程的引用;
  2. 獲取當(dāng)前線程內(nèi)部的 ThreadLocalMap;
  3. 如果 map 存在,則獲取當(dāng)前 ThreadLocal 對(duì)應(yīng)的 value 值;
  4. 如果 map 不存在或者找不到 value 值,則調(diào)用 setInitialValue() 進(jìn)行初始化。

get 方法的時(shí)序圖如下所示:

并發(fā)編程之ThreadLocal深入理解

其中每個(gè) Thread 的 ThreadLocalMap 以 threadLocal 作為 key,保存自己的線程的 value副本,也就是保存在每個(gè)線程中,并沒有保存在 ThreadLocal 對(duì)象中。

小結(jié)

通過對(duì)源碼的分析,現(xiàn)在我們來總結(jié)一下:

  1. 每個(gè)Thread維護(hù)著一個(gè)ThreadLocalMap的引用;
  2. ThreadLocalMap是ThreadLocal的內(nèi)部類,用Entry來進(jìn)行存儲(chǔ);
  3. ThreadLocal創(chuàng)建的副本是存儲(chǔ)在自己的threadLocals中的,也就是自己的ThreadLocalMap;
  4. ThreadLocalMap的鍵值為ThreadLocal對(duì)象,而且可以有多個(gè)threadLocal變量,因此保存在map中;
  5. 在進(jìn)行g(shù)et之前,必須先set,否則會(huì)報(bào)空指針異常,當(dāng)然也可以初始化一個(gè),但是必須重寫initialValue()方法;
  6. ThreadLocal本身并不存儲(chǔ)值,它只是作為一個(gè)key來讓線程從ThreadLocalMap獲取value。

ThreadLocal 應(yīng)用場(chǎng)景

ThreadLocal 的特性也導(dǎo)致了應(yīng)用場(chǎng)景比較廣泛,主要的應(yīng)用場(chǎng)景如下:

  • 線程間數(shù)據(jù)隔離,各線程的 ThreadLocal 互不影響
  • 方便同一個(gè)線程使用某一對(duì)象,避免不必要的參數(shù)傳遞
  • 全鏈路追蹤中的 traceId 或者流程引擎中上下文的傳遞一般采用 ThreadLocal
  • Spring 事務(wù)管理器采用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的實(shí)現(xiàn)使用了 ThreadLocal

總結(jié):面試常見問題

Thread、ThreadLocal 以及 ThreadLocalMap關(guān)系

通過對(duì)以上源碼的分析,Thread、ThreadLocal 以及 ThreadLocalMap 的關(guān)系有了進(jìn)一步的理解,我們?cè)偻ㄟ^一張圖來總結(jié)下:

并發(fā)編程之ThreadLocal深入理解

ThreadLocal 是如何實(shí)現(xiàn)線程隔離的呢?

ThreadLocal 是如何做到線程數(shù)據(jù)隔離,前面源碼分析 ThreadLocal 的 set 方法已經(jīng)分析過,這里我們?cè)倏偨Y(jié)一下:

ThreadLocal之所以能達(dá)到變量的線程隔離,其實(shí)就是每個(gè)線程都有一個(gè)自己的ThreadLocalMap對(duì)象來存儲(chǔ)同一個(gè)threadLocal實(shí)例set的值,而取值的時(shí)候也是根據(jù)同一個(gè)threadLocal實(shí)例去自己的ThreadLocalMap里面找,自然就互不影響了,從而達(dá)到線程隔離的目的。如下圖所示:

并發(fā)編程之ThreadLocal深入理解

ThreadLocal內(nèi)存泄漏問題

ThreadLocal 在沒有外部強(qiáng)引用時(shí),發(fā)生 GC時(shí)會(huì)被回收,那么 ThreadLocalMap 中保存的 key 值就變成了 null,而 Entry 又被 threadLocalMap 對(duì)象引用,threadLocalMap 對(duì)象又被 Thread 對(duì)象所引用,那么當(dāng) Thread 一直不終結(jié)的話,value 對(duì)象就會(huì)一直存在于內(nèi)存中,也就導(dǎo)致了內(nèi)存泄漏,直至 Thread 被銷毀后,才會(huì)被回收。我們通過一張圖來理解下:

并發(fā)編程之ThreadLocal深入理解

ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果沒有手動(dòng)刪除對(duì)應(yīng)key就會(huì)導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?/strong>

那么如何避免內(nèi)存泄漏呢?

在使用完 ThreadLocal 變量后,需要我們手動(dòng) remove 掉,防止 ThreadLocalMap 中的 Entry 一直保持對(duì) value 的強(qiáng)引用,導(dǎo)致 value 不能被回收。

PS:以上代碼提交在 Github :

https://github.com/Niuh-Study/niuh-juc-final.git

原文地址:https://www.toutiao.com/i6904646162406654467/

延伸 · 閱讀

精彩推薦
Weibo Article 1 Weibo Article 2 Weibo Article 3 Weibo Article 4 Weibo Article 5 Weibo Article 6 Weibo Article 7 Weibo Article 8 Weibo Article 9 Weibo Article 10 Weibo Article 11 Weibo Article 12 Weibo Article 13 Weibo Article 14 Weibo Article 15 Weibo Article 16 Weibo Article 17 Weibo Article 18 Weibo Article 19 Weibo Article 20 Weibo Article 21 Weibo Article 22 Weibo Article 23 Weibo Article 24 Weibo Article 25 Weibo Article 26 Weibo Article 27 Weibo Article 28 Weibo Article 29 Weibo Article 30 Weibo Article 31 Weibo Article 32 Weibo Article 33 Weibo Article 34 Weibo Article 35 Weibo Article 36 Weibo Article 37 Weibo Article 38 Weibo Article 39 Weibo Article 40
主站蜘蛛池模板: 国产精品成av人在线视午夜片 | 在线a免费 | 久久久在线 | 中文字幕不卡 | 精品一区二区三区视频 | 久久久国产一区二区 | 久久久久99 | 亚洲在线播放 | 精品成人久久 | 国产一区免费视频 | 一级片网| 伊人精品视频 | 日本中文字幕在线视频 | 日韩综合视频在线观看 | 久久丁香 | 国产大片在线观看 | 日韩毛片在线观看 | 久久精品亚洲精品 | 成人影音 | 成人午夜视频在线观看 | 欧美一区二区三区xxxx监狱 | 玖玖精品视频 | 一 级 黄 色 片免费网站 | 激情五月激情 | 国产美女自拍视频 | 69久久久| 亚洲人免费 | 骚片网站| 欧美操操操 | 99亚洲精品 | 欧美日韩三级在线 | 国产1级片 | 日韩无在线 | 精品久| 中文字幕在线观看一区二区三区 | 国产成人精品一区二 | 黄色精品网站 | 一本大道色卡1卡2卡3 | 国产精品二区三区 | 欧美一级黄色片免费看 | 中文字幕在线一区二区三区 |