java 併發(七)--- ThreadLocal

来源:https://www.cnblogs.com/Benjious/archive/2018/12/22/10162864.html
-Advertisement-
Play Games

文章部分圖片來自參考資料 ThreadLocal 概述 ThreadLocal 線程本地變數 ,是一個工具,可以讓多個線程保持一個變數的副本,那麼每個線程可以訪問自己內部的副本變數。 ReentranReadWriteLock中。 ThreadLocal 結構圖裡面看到有兩個內部類,一個 Suppl... ...


     文章部分圖片來自參考資料

 

ThreadLocal 概述    

         ThreadLocal 線程本地變數 ,是一個工具,可以讓多個線程保持一個變數的副本,那麼每個線程可以訪問自己內部的副本變數。

ReentranReadWriteLock中。

        ThrealLocal

           ThreadLocal 結構圖裡面看到有兩個內部類,一個 SuppliedThreadLocal , 一個ThreadLocalMap 。下麵用一張圖來說明線程使用的示意圖。可以看到每個Thread有個 ThreadLocalMap ,然後裡面由hash值分列的的數組 Entry[] 。Entry 數據結構就是圖中淡綠色框內所示。

 

threadlocal

 

ThreadLocal  源碼分析

         ThreadLocal 下文簡稱 TL, TL最常見的方法就是 get 和 set 了。

  1 public void set(T value) {
  2         Thread t = Thread.currentThread();
  3         ThreadLocalMap map = getMap(t);
  4         if (map != null)
  5             map.set(this, value);
  6         else
  7             createMap(t, value);
  8 }
 
  1     public T get() {
  2         Thread t = Thread.currentThread();
  3         ThreadLocalMap map = getMap(t);
  4         if (map != null) {
  5             ThreadLocalMap.Entry e = map.getEntry(this);
  6             if (e != null) {
  7                 @SuppressWarnings("unchecked")
  8                 T result = (T)e.value;
  9                 return result;
 10             }
 11         }
 12         return setInitialValue();
 13     }
 
  1     ThreadLocalMap getMap(Thread t) {
  2         return t.threadLocals;
  3     }

 

  1  ThreadLocal.ThreadLocalMap threadLocals = null;

         可以看到thread 內部中持有TL的內部類變數。我們來看一下 ThreadLocalMap, threadLocalMap 內部定義一個類,Entry 類。這是threadLocalMap  內的變數

  1 static class ThreadLocalMap {
  2     /**
  3      * The initial capacity -- MUST be a power of two.
  4      */
  5     private static final int INITIAL_CAPACITY = 16;
  6 
  7     /**
  8      * The table, resized as necessary.
  9      * table.length MUST always be a power of two.
 10      */
 11     private Entry[] table;
 12 
 13     /**
 14      * The number of entries in the table.
 15      */
 16     private int size = 0;
 17 
 18     /**
 19      * The next size value at which to resize.
 20      */
 21     private int threshold; // Default to 0
 22 }
 23 
  1  static class Entry extends WeakReference<ThreadLocal<?>> {
  2             /** The value associated with this ThreadLocal. */
  3             Object value;
  4 
  5             Entry(ThreadLocal<?> k, Object v) {
  6                 super(k);
  7                 value = v;
  8             }
  9 }

         我們看到 TL 的set 方法實際就是調用了 ThreadLocalMap 的set 方法。

  1  private void set(ThreadLocal<?> key, Object value) {
  2 
  3             // We don't use a fast path as with get() because it is at
  4             // least as common to use set() to create new entries as
  5             // it is to replace existing ones, in which case, a fast
  6             // path would fail more often than not.
  7 
  8             Entry[] tab = table;
  9             int len = tab.length;
 10             int i = key.threadLocalHashCode & (len-1);
 11 
 12             for (Entry e = tab[i];
 13                  e != null;
 14                  e = tab[i = nextIndex(i, len)]) {
 15                 ThreadLocal<?> k = e.get();
 16 
 17             	//找到相同的 key 
 18                 if (k == key) {
 19                     e.value = value;
 20                     return;
 21                 }
 22 
 23                 //某個key失效
 24                 if (k == null) {
 25                     replaceStaleEntry(key, value, i);
 26                     return;
 27                 }
 28             }
 29 
 30            	//走到這裡必定是退出了迴圈,即是遇到空的 entry ,直接放在空的地方,檢查是否需要擴容,重新 hash 
 31             tab[i] = new Entry(key, value);
 32             int sz = ++size;
 33             if (!cleanSomeSlots(i, sz) && sz >= threshold)
 34                 rehash();
 35 }
 36 
 37 
 38   //   這個方法是替代某些失效的entry ,最終的值會放在 table[staleSlot]
 39   //  slotToExpunge 這個變數從名字上可以看出就是需要擦洗的 slot (指的是某個位置)
 40  private void replaceStaleEntry(ThreadLocal<?> key, Object value,
 41                                        int staleSlot) {
 42             Entry[] tab = table;
 43             int len = tab.length;
 44             Entry e;
 45 
 46             // Back up to check for prior stale entry in current run.
 47             // We clean out whole runs at a time to avoid continual
 48             // incremental rehashing due to garbage collector freeing
 49             // up refs in bunches (i.e., whenever the collector runs).
 50             //  向前找是否有失效節點,如果有做一下標記,即是為 slotToExpunge 賦值
 51             int slotToExpunge = staleSlot;
 52             for (int i = prevIndex(staleSlot, len);
 53                  (e = tab[i]) != null;
 54                  i = prevIndex(i, len))
 55                 if (e.get() == null)
 56                     slotToExpunge = i;
 57 
 58             // Find either the key or trailing null slot of run, whichever
 59             // occurs first
 60             //  向後尋找是否有相同的 key
 61             for (int i = nextIndex(staleSlot, len);
 62                  (e = tab[i]) != null;
 63                  i = nextIndex(i, len)) {
 64                 ThreadLocal<?> k = e.get();
 65 
 66                 // If we find key, then we need to swap it
 67                 // with the stale entry to maintain hash table order.
 68                 // The newly stale slot, or any other stale slot
 69                 // encountered above it, can then be sent to expungeStaleEntry
 70                 // to remove or rehash all of the other entries in run.
 71             	//  找到相同的值,交換位置到 tab[staleSlot]
 72                 if (k == key) {
 73                     e.value = value;
 74 
 75                     tab[i] = tab[staleSlot];
 76                     tab[staleSlot] = e;
 77 
 78                     // Start expunge at preceding stale entry if it exists
 79                     // 擦洗失效值
 80                     if (slotToExpunge == staleSlot)
 81                         slotToExpunge = i;
 82                     cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
 83                     return;
 84                 }
 85 
 86                 // If we didn't find stale entry on backward scan, the
 87                 // first stale entry seen while scanning for key is the
 88                 // first still present in the run.
 89                 if (k == null && slotToExpunge == staleSlot)
 90                     slotToExpunge = i;
 91             }
 92 
 93             // If key not found, put new entry in stale slot
 94             //找不到值會放在 tab[staleSlot] ,即原來失效值的位置上
 95             tab[staleSlot].value = null;
 96             tab[staleSlot] = new Entry(key, value);
 97 
 98             // If there are any other stale entries in run, expunge them
 99             // 擦洗失效值
100             if (slotToExpunge != staleSlot)
101                 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
102 }
103 

       

TML

 

TML2       

     可以看到我們在 set 的時候,TL內會檢查是否存在失效值。也可以看到 ThreadLocalMap 的Hash 中解決衝突的方式只是簡單的向下尋找空的位置,即線性探測,這樣的效率比較低,所以建議 :

 

         每個線程只存一個變數,這樣的話所有的線程存放到map中的Key都是相同的ThreadLocal,如果一個線程要保存多個變數,就需要創建多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增加Hash衝突的可能。

 

        下麵看一下 get 方法,不難。

  1 // ThreadLocalMap
  2 private Entry getEntry(ThreadLocal<?> key) {
  3             int i = key.threadLocalHashCode & (table.length - 1);
  4             Entry e = table[i];
  5             if (e != null && e.get() == key)
  6                 return e;
  7             else
  8                 return getEntryAfterMiss(key, i, e);
  9 }
  1        private Entry getEntry(ThreadLocal<?> key) {
  2             int i = key.threadLocalHashCode & (table.length - 1);
  3             Entry e = table[i];
  4             if (e != null && e.get() == key)
  5                 return e;
  6             else
  7             	//獲取的時候出現失效的entry
  8                 return getEntryAfterMiss(key, i, e);
  9         }
 10 
 11 
 12         // 往後找,失效的值擦洗掉,沒有就返回 Null
 13         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
 14             Entry[] tab = table;
 15             int len = tab.length;
 16 
 17             while (e != null) {
 18                 ThreadLocal<?> k = e.get();
 19                 if (k == key)
 20                     return e;
 21                 if (k == null)
 22                     expungeStaleEntry(i);
 23                 else
 24                     i = nextIndex(i, len);
 25                 e = tab[i];
 26             }
 27             return null;
 28         }

 

ThreadLocalMap 的 key 失效

           ThreadLocalMap下文簡稱  TLM 。

  1 static class Entry extends WeakReference<ThreadLocal<?>> {
  2             /** The value associated with this ThreadLocal. */
  3             Object value;
  4 
  5             Entry(ThreadLocal<?> k, Object v) {
  6                 super(k);
  7                 value = v;
  8             }
  9         }

         可以看到 Entry 繼承 WeakReference (弱引用)。ThreadLocal在沒有外部對象強引用時,發生GC時弱引用Key會被回收,而Value不會回收,如果創建ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,於是這就存在一條強引用鏈的關係一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成記憶體泄漏。

 

 

tlm

         

              我們從源碼中也可以看到在 get 和 set 等方法都有檢查失效值的操作,同時當我們使用TL時,某個線程不再需要某個值的時候應該調用 remove 方法,下麵代碼中 e.clear() 這一句實際是調用了弱引用的 clear 方法,實現對對象的回收。

  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             }
 14         }
  1     /**
  2      * Clears this reference object.  Invoking this method will not cause this
  3      * object to be enqueued.
  4      *
  5      * <p> This method is invoked only by Java code; when the garbage collector
  6      * clears references it does so directly, without invoking this method.
  7      */
  8     public void clear() {
  9         this.referent = null;
 10     }

          

         我們來看一下weakReference 表示弱引用,java中有四種引用類型,強引用,弱引用,軟引用,虛引用。

 

       在Java語言中, 當一個對象o被創建時, 它被放在Heap里. 當GC運行的時候, 如果發現沒有任何引用指向o, o就會被回收以騰出記憶體空間. 也就是說, 一個對象被回收, 必須滿足兩個條件:

  • 沒有任何引用指向它

  • GC被運行.

         

  1 DemoA a=new DemoA();
  2 DemoB b=new DemoB(a);

        假如有下麵代碼,如果我們增加一行代碼來將a對象的引用設置為null,當一個對象不再被其他對象引用的時候,是會被GC回收的,但是對於這個場景來說,即時是a=null,也不可能被回收,因為DemoB依賴DemoA,這個時候是可能造成記憶體泄漏的。

  1 DemoA a=new DemoA();
  2 DemoB b=new DemoB(a);
  3 a=null;

         通過弱引用,有兩個方法可以避免這樣的問題。

  1 //方法1
  2 DemoA a=new DemoA();
  3 DemoB b=new DemoB(a);
  4 a=null;
  5 b=null;
  6 //方法2
  7 DemoA a=new DemoA();
  8 WeakReference b=new WeakReference(a);
  9 a=null;
 10 

        對於方法2來說,DemoA只是被弱引用依賴,假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除所有指向該對象的弱引用,然後把這個弱可達對象標記為可終結(finalizable)的,這樣它隨後就會被回收。

 

       我們可以設想b就是ThreadLocal ,試想一下如果這裡沒有使用弱引用,意味著ThreadLocal的生命周期和線程是強綁定,只要線程沒有銷毀,那麼ThreadLocal一直無法回收。而使用弱引用以後,當ThreadLocal被回收時,由於Entry的key是弱引用,不會影響ThreadLocal的回收防止記憶體泄漏,同時,在後續的源碼分析中會看到,ThreadLocalMap本身的垃圾清理會用到這一個好處,方便對無效的Entry進行回收。

 

 

其實我們從源碼分析可以看到,ThreadLocalMap是做了防護措施的

  • 首先從ThreadLocal的直接索引位置(通過

    ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不為null並且key相同則返回e

  • 如果e為null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值為null,則擦除該位置的Entry,否則繼續向下一個位置查詢

 

 

Entry 的 Hash 值

         如何實現一個線程多個ThreadLocal對象,每一個ThreadLocal對象是如何區分的呢? 

  1 void createMap(Thread t, T firstValue) {
  2        t.threadLocals = new ThreadLocalMap(this, firstValue);
  3 }
  1 static class ThreadLocalMap {
  2      static class Entry extends WeakReference<ThreadLocal<?>> {
  3 
  4        /** The value associated with this ThreadLocal. */
  5         Object value;
  6 
  7         Entry(ThreadLocal<?> k, Object v) {
  8                 super(k);
  9                value = v;
 10         }
 11      }
 12 
 13      ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 14 	   //構造一個Entry數組,並設置初始大小
 15            table = new Entry[INITIAL_CAPACITY];
 16            //計算Entry數據下標
 17            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 18 	   //將`firstValue`存入到指定的table下標中
 19            table[i] = new Entry(firstKey, firstValue);
 20            size = 1;//設置節點長度為1
 21            setThreshold(INITIAL_CAPACITY); //設置擴容的閾值
 22       }
 23 //...省略部分代碼
 24 }
 25 
 26 
  1 private final int threadLocalHashCode = nextHashCode();
  2 private static AtomicInteger nextHashCode = new AtomicInteger();
  3 private static final int HASH_INCREMENT = 0x61c88647;
  4 
  5 private static int nextHashCode() {
  6     return nextHashCode.getAndAdd(HASH_INCREMENT);
  7 }

        那為什麼要使用到 0x61c88647 這個值呢? 我們首先要明白一點,散列的目的是使數據分佈更加均勻。那麼這個數字的使用必定會達到這個目的。

 

魔數0x61c88647的選取和斐波那契散列有關,0x61c88647對應的十進位為1640531527。而斐波那契散列的乘數可以用 (long)((1L<<31)*(Math.sqrt(5)-1)); 如果把這個值給轉為帶符號的int,則會得到-1640531527。也就是說(long)((1L<<31)*(Math.sqrt(5)-1));得到的結果就是1640531527,也就是魔數0x61c88647

      

建議

  • 將ThreadLocal變數定義成private static的,這樣的話ThreadLocal的生命周期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止記憶體泄露

  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

 

小提示

          線上程池中使用ThreadLocal ,有可能會出現數據混淆的情況,原因是數據沒及時清理,線程放回線程池中又被拿出來使用。

 

 

參考資料 :


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • java 面試中單例模式基本都是必考的,都有最推薦的方式,也不知道問來幹嘛。下麵記錄一下 ...
  • builder模式,目的在於:抽離複雜對象的構造函數,讓我們可以通過多種方法的排列組合構建複雜的對象。如果構造器參數過多,可以考慮 builder 模式 ...
  • 上一篇用鏈表實現了stack,這篇我們採用數組來存儲數據,數組更容易理解,直接貼代碼 第一、代碼實現 1 #pragma once 2 #include <iostream> 3 using namespace std; 4 template <typename T> class StackArra ...
  • 有時,變數的取值只在一個有限的集合內。針對這種情況,可以使用枚舉類型。此外,使用枚舉值來代替直接使用字面量可以讓程式更加易於閱讀和維護。本文將從枚舉類型的定義以及如何使用枚舉類型兩個方面對枚舉類型進行詳細的介紹。 ...
  • C++是一門電腦編程語言,G++不是語言,是一款編譯器中編譯C++程式的命令而已。 不同的編譯器,會對代碼做出一些不同的優化 比如說: a++; 和 ++a; 如果從標準C的角度去理解。a++這個語句等同於 a = a + 1 也就是說,我是先調用,再自增。在調用過程中,會申請一個新的數據地址,用 ...
  • 1、Spring Boot 簡介 簡化Spring應用開發的一個框架 整個Spring技術棧的一個大整合 J2EE開發的一站式解決方案 2、微服務 2014, martin fowler 微服務:架構服務 (服務微化) 一個應用應該是一組小型服務;可以通過HTTP的方式進行互通 單體應用:ALL I ...
  • 因為之前一直在項目中使用django, 所以在學習Flask的過程中, 難免對吧django和Flask進行對比, 這一次我發現Flask中的request和session並沒有想象的那麼簡單, 所以就讀了一下這一部分的源碼, 讀完之後對整個過程理解了一番, 好像並沒有get到Flask的開發這個這 ...
  • 第一、基本概念 棧中的元素遵守“先進後出”的原則(LIFO,Last In First Out) 只能在棧頂進行插入和刪除操作 壓棧(或推入、進棧)即push,將數據放入棧頂並將棧頂指針加一 出棧(或彈出)即pop,將數據從棧頂刪除並將棧頂指針減一 棧的基本操作有:pop,push,判斷空,獲取棧頂 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...