源碼 | 完全理解ThreadLocal---Java併發編程系列(一)

先備知識

Java中不一樣的引用類型

在Java中根據垃圾回收的方式不一樣,引用按照對象生命週期的長短分爲四種,由高到低分別爲強引用、軟引用、弱引用和虛引用。java

強引用

Java中默認的引用類型,一個對象若是具備強引用那麼就沒有資格被垃圾回收。 數組

VfCm5t.png

軟引用

一個對象若是隻具備軟引用,當JVM內存充足的時候和強引用並沒有區別,那麼當JVM內存不足的時候,這個對象就會被垃圾回收。軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用。若是軟引用所引用對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。 緩存

VfC1Kg.png

弱引用

若是一個對象只具備弱引用(即不具備強引用,軟引用,虛引用),那麼這個對象會被垃圾回收器標記回收。弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。弱引用指向的對象能夠經過弱引用的get方法得到,由於弱引用不能阻擋垃圾回收器對其回收,因此當弱引用指向的對象被GC的時候get方法會返回null。 微信

VfCaGV.png

虛引用

虛引用不會影響對象的生命週期,惟一用處就是能在對象被GC時收到系統通知,JAVA中用PhantomReference來實現虛引用。虛引用指向的對象十分脆弱,咱們不能夠經過get方法來獲得其指向的對象。dom

ThreadLocal簡介

ThreadLocal是什麼?

首先ThreadLocal是一個用於線程建立本身線程本地變量的類。這個變量相對於本線程是全局的,相對於其餘線程是隔離的,也就是說在不一樣的線程之間獨立存在,一個線程沒法訪問和修改其餘線程的線程本地變量。即使兩個線程執行同一段代碼,在代碼中都使用了ThreadLocal來建立線程本地變量,那它們也只能看到並訪問本身線程的本地變量,並不能相互看到對方的線程本地變量。ide

如何使用?

Talk is cheap,show me the code。函數

咱們先經過一個實際的例子看一下ThreadLocal的使用。性能

假設有一個商城,客戶下發一個訂單,商城會分紅多個步驟來處理這個訂單(查庫存,配貨等),商城爲每一個訂單分配一個惟一標識OrderID,而且在訂單的各個處理步驟中都應該被隨時讀取。咱們對於每一個客戶的訂單處理new一個線程來表示,實際處理的步驟省略掉只是打印OrderID。大數據

public class OrderIdHolder {
    public static final ThreadLocal<String> CURRENT_ORDERID = new ThreadLocal();

    static String getCurrentOrderId() {
        return CURRENT_ORDERID.get();
    }

    static void setCurrentOrderId(String Id) {
        CURRENT_ORDERID.set(Id);
    }

    static void remove() {
        CURRENT_ORDERID.remove();
    }
}

public class OrderProcessingThread extends Thread {
    Random random = new Random();

    OrderProcessingThread(String name) {
       super(name);
    }

    @Override
    public void run() {
        OrderIdHolder.setCurrentOrderId(getName() +" " + random.nextInt(100));
        /*注意這裏咱們並無顯式的傳遞OrderId*/
        BusinessService businessService = new BusinessService();
        businessService.checkInventory();
        businessService.ship();
        OrderIdHolder.remove();
    }

    public static void main(String args[]) {
        Thread threadOne = new OrderProcessingThread("ThreadA");
        threadOne.start();

        Thread threadTwo = new OrderProcessingThread("ThreadB");
        threadTwo.start();
    }
}

public class BusinessService {
    public void checkInventory() {
        System.out.println("checkInventory " + OrderIdHolder.getCurrentOrderId());
    }

    public void ship() {
        System.out.println("ship " + OrderIdHolder.getCurrentOrderId());
    }
}
複製代碼

結果:this

checkInventory ThreadB 18
checkInventory ThreadA 42
ship ThreadA 42
ship ThreadB 18
複製代碼

如上所示,雖然咱們並無顯式的將OrderId傳遞到checkInventory和ship方法內,可是同一個訂單處理(同一個線程)的兩個方法得到的OrderId均相同,可是不一樣的訂單處理(不一樣線程)的OrderId是不一樣的。能夠看到ThreadLocal變量是每一個線程「獨自持有一份兒的」,兩個線程實際上是有兩份兒不同的ThreadLocal變量。

能夠解決什麼問題?

從上面的例子能夠體會到,當一個實例,不被容許在多個線程間共享,可是對於每一個線程來講不一樣的類與方法都須要共享並常常訪問這個實例的時候,應該使用ThreadLocal

ThreadLocal核心源代碼解析

從對開發者暴露的set方法入手

你可能會有疑問,咱們存儲變量時明明是隻有一個CURRENT_ORDERID(ThreadLocal),爲何每一個線程會本身有一份兒線程本地變量呢?下面咱們一塊兒揭開ThreadLocal的神祕面紗。

咱們直接從ThreadLocal使用時的核心方法set入手。

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

    ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
    }
}

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
複製代碼

調用set方法時,先是取得了當前的線程,而後調用getMap方法,取得了一個Map,從這裏能夠看出ThreadLocal自己並不存儲變量的值,數據實際存放在Thread內的一個Map裏面,也就是說數據實際都是存放在各個線程自己的,使用者調用ThreadLocal的set()方法其實最終都是對這個Map進行操做的。ThreadLocal只是爲咱們操做這個Map提供了一個便捷入口。能夠看到ThreadLocalMap的初始值是null,當第一次經過ThreadLocal的set方法或get方法試圖訪問操做這個Map時會調用createMap(t, value)進行初始化工做。

public class ThreadLocal<T> {
    ……
    private final int threadLocalHashCode = nextHashCode();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private Entry[] table;
    private int size = 0;
    private int threshold;

    private static int nextHashCode() {
       return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    ……
    static class ThreadLocalMap {
      private Entry[] table;
      private int size = 0;
      private int threshold;
      private static final int INITIAL_CAPACITY = 16;

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

      private void setThreshold(int len) {
           threshold = len * 2 / 3;
       }
    }
}
複製代碼

上面的代碼中能夠看出ThreadLocalMap其實就是一個依賴數組實現,定製化的HashTable。ThreadLocal對象實例做爲Key用於定位數據實際在Entry數組中的下標,下標的值爲ThreadLocal對象的threadLocalHashCode通過位運算取模獲得(不太清楚原理的同窗請參考https://blog.csdn.net/actionzh/article/details/78976082)。在下標處放入相應數據後,把當前Entry數組已存放數據的個數(size)設置爲1,並把Threshold設置爲當前容量的2/3,這個值在進行擴容時會做爲判斷條件使用。 除了第一次訪問操做Map調用ThreadLocalMap的createMap(t, value)初始化ThreadLocalMap(其實是ThreadLocalMap底層的Entry數組),之後都會調用ThreadLocalMap的set方法存放數據。

static class ThreadLocalMap {
      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)]) {
                // 註釋1:Entry的get方法
                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();
      }

      static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
     }
}
複製代碼

上面代碼中的註釋1處咱們看到調用了Entry對象的get方法,返回的類型是ThreadLocal,那麼這裏是什麼意思呢?咱們先來看一下Entry這個類。能夠看到繼承了WeakReference這個類,那麼能夠看到Entry的key是一個指向了ThreadLocal對象的弱引用。若是指向的對象被GC掉了(前面說過,弱引用是不會影響其指向對象的GC的),那麼Entry對象的get方法就會返回null,能夠由此來判斷其指向的ThreadLocal對象是否已經無用被用戶「棄用」。

上面這段代碼整體邏輯比較簡單,先根據ThreadLocal對象計算出以此爲key的Entry應該放置在Entry數組中的Index,若是這個Index處沒有Entry,直接放置,若是已經放置了Entry也即slot不爲空,那麼就說明兩個Entry的key映射到了一個地方,也就是散列表產生了衝突,此時採用線性探測法解決衝突來探測空的slot。探測的過程當中,若是查找到了目標key的Entry,直接替換value爲咱們的目標value便可。比較重點的地方是線性探測的過程當中若是遇到了位置i此slot處的Entry的key指向的ThreadLocal已經被GC掉了,那麼就將i與待插入的Entry做爲參數傳遞給replaceStaleEntry方法,並執行而後直接return。

replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)
複製代碼

replaceStaleEntry方法具體都作了什麼呢?replaceStaleEntry會將做爲其參數傳遞來的Entry存放在Entry數組的staleSlot處,並會清除夾在staleSlot先後兩個null之間的一連串Entry中全部key爲null(即指向的ThreadLocal已經被GC)的Entry。聽起來有些繞,爲了下降描述的複雜度,引入兩個名詞。

  • run --- 在Entry數組中夾在兩個null之間的一連串Entry。
  • Stale Entry --- Entry的key指向的ThreadLocal已經被GC,這個時候ThreadLocal已經不存在了,那麼這個ThreadLocal對應存放的數據Entry已經沒有意義天然要被GC,因此形象地來講就是Stale Entry。

如今從新表述一下replaceStaleEntry的做用:將參數傳遞來的Entry存放在Entry數組的staleSlot(函數第三個參數)處,並清除Entry數組中staleSlot所在run中全部的Stale Entry。 代碼讀到這裏咱們能夠大體畫出ThreadLocal的總體原理圖了:

VfCIqH.png

丟掉冗餘,才能高效---清道夫replaceStaleEntry

下面咱們就來看一下這個replaceStaleEntry的具體實現。

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            int slotToExpunge = staleSlot;

            // 第一階段
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // 第二階段
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 第三階段
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            if (slotToExpunge != staleSlot)
                // 第三階段
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
複製代碼

根據ThreadLocalMap的set方法中replaceStaleEntry調用的狀況,這三個參數分別表明要插入Entry的key,value以及線性探測法解決哈希衝突掃描的過程當中遇到的第一個StaleEntry的Index。 進入方法內部查看具體的實現,能夠發現replaceStaleEntry自己只負責了將Entry放到staleSlot處,實際上當前run的staleEntry清理操做交給了expungeStaleEntry方法,replaceStaleEntry內只是爲expungeStaleEntry方法的調用作了準備工做。 簡單來講此方法主要作了兩件事:

  • 將待插入的Entry放到StaleEntry處 從staleSlot處開始向後掃描staleSlot當前所在run,若是發現具備目標key的Entry將value設置成將要插入的Entry的value並將此Entry與staleSlot處的staleEntry交換,若是沒有發現,那麼直接new一個Entry放到staleSlot處。
  • 肯定進行StaleEntry清掃工做的起始Index->slotToExpunge,並將其做爲參數傳遞給expungeStaleEntry方法進行StaleEntry清掃 全面掃描staleSlot所在run除了staleSlot以外(由於無論怎樣,調用expungeStaleEntry前,staleSlot處都會填充Entry再也不是staleEntry了)的第一個staleEntry的Index->slotToExpunge,並傳遞給expungeStaleEntry方法進行當前run的從slotToExpunge開始對staleEntry的大清掃。若是沒有找到除了staleSlot處的Entry以外的staleEntry那麼就不進行清掃工做。 下面咱們經過幾個例子,直觀地看一下大體的處理流程(下面的例子並不是要窮舉全部狀況,旨在幫助讀者經過關鍵例子體會代碼的執行邏輯)。 爲了後續圖片表述的無歧義性,這裏再約定幾個畫圖規則。
    VfCORf.png
  • 上圖表示Entry數組
  • 每一個格子(Slot)裏面: NULL:表示空Slot。 空心圓:StaleEntry。 黑色實心圓:表示不爲NULL也不是StaleEntry的正常Entry。 黑色實心圓有Key標誌:表示不爲NULL也不是StaleEntry的正常Entry,而且Key與所要插入的Entry的Key相同。 replaceStaleEntry的過程大體能夠表示爲以下三個階段(下面的內容請結合上面的代碼裏面的註釋一塊兒看):

replaceStaleEntry方法執行前初始狀態

狀況1:

VfPJyD.png
狀況2:
VfP2wj.png
狀況3:
VfP4f0.png
狀況4:
VfPT6U.png
第一階段 狀況1: 在run中從staleSlot處出發向前掃描,若是發現staleEntry那麼將掃描過程當中排在最前面的staleEntry的Index賦值給slotToExpunge。
Vfi9XD.png
狀況2,3,4: 在run中從staleSlot處出發向前掃描,沒有發現staleEntry,不作任何事情。

第二階段

接下來在run中從staleSlot處出發向後掃描,掃描過程當中對於每個slot內的數據:

1.先判斷是不是具備目標key的Entry,若是是,將其value設置成將要插入的Entry的value並與staleSlot處的staleEntry交換(注意交換後當前slot處的Entry就是staleEntry了)。交換後判斷slotToExpunge == staleSlot成立則說明此run內當前Slot位置前並沒有staleEntry。當前slot的Index就是當前run中的第一個staleEntry的Index,也即後續清掃工做的起始Index,將其賦值給slotToExpunge。若是slotToExpunge!=staleSlot說明此時slotToExpunge的值已是當前run中第一個staleEntry的Index了,那就不對slotToExpunge值作更改。slotToExpunge被肯定後,中止繼續向後掃描,進入到第三階段將slotToExpunge傳遞給清理函數進行staleEntry的清掃工做,而後return。

2.若是當前slot內的數據不是具備目標key的Entry,判斷當前slot內的Entry若是知足是staleEntry而且slotToExpunge == staleSlot,那麼就表明當前Entry的Index是當前run除了staleSlot外的第一個staleEntry的Index,也即後續清掃工做的起始Index,將其賦值給slotToExpunge。 在第二階段的末尾,若是掃描過程當中沒有掃描到具備目標key的Entry,那麼直接將要插入的Entry放到staleSlot處,若是此時slotToExpunge!=staleSlot, 說明當前run中有staleEntry而且slotToExpunge是第一個staleEntry,也即後續清掃工做的起始Index,那麼進入第三階段將slotToExpunge參數傳遞給清理函數進行staleEntry的清掃工做。若是slotToExpunge == staleSlot則證實當前run沒有須要清理的staleEntry就不進入第三階段。

總結一下:第二階段的任務就是a.在第一階段從staleSlot處向前掃描的基礎上,向後掃描最終肯定進行StaleEntry清掃工做的起始Index->slotToExpunge。 b.將待插入的Entry放到StaleEntry處。c.根據slotToExpung的值與staleSlot的值的相等關係來判斷是否進入第三階段。 :slotToExpunge值的更改,都是判斷slotToExpunge==staleSlot成立後才進行的,由於初始值slotToExpunge就是等於staleSlot的,這樣能夠保證slotToExpunge的值只有在碰見當前run內除了staleSlot處外第一個staleEntry的時候纔會更改,保證了slotToExpunge的值是當前run中的第一個staleEntry的Index,也即後續清掃工做的起始Index。

狀況1: 按照上述步驟,狀況1的第二階段如圖所示:(注意一點,掃描到具備目標key的Entry後,這個階段的三個任務都已經完成,將不繼續向後掃描,直接進入第三階段)。

VfiAAA.png
狀況2: slotToExpunge會變成交換前的目標key所在位置,此時slotToExpunge爲當前run的第一個StaleEntry的Index,以下圖所示:(一樣掃描到具備目標key的Entry後,不繼續向後掃描,直接進入第三階段。)
Vfim1f.png
狀況3: 在當前run從satleSlot向後掃描,slotToExpunge置爲當前run除了staleSlot處外第一個StaleEntry的Index,並將待插入的Entry放到staleSlot位置。經判斷staleSlot不等於slotToExpunge,表示當前run有staleEntry須要被清理,將其傳遞給expungeStaleEntry清掃方法,進行第三階段staleEntry大清掃工做。如圖所示:
Vfi3As.png
狀況4: 沒有掃描到具備目標key的Entry以及staleEntry,將待插入的Entry放到staleSlot位置。判斷一下staleSlot等於slotToExpunge,意味着當前的run並無staleEntry須要清理(staleSlot處已經放置Entry),說明當前run很乾淨不用清掃,不作任何操做,不進入第三階段。如圖所示:
VfiGhq.png
第三階段

狀況1,狀況2,狀況3: 執行expungeStaleEntry清掃方法,進行staleEntry大清掃工做。 狀況4: 未進入第三階段。

清掃工做的具體執行者---清道夫expungeStaleEntry

前面說過,replaceStaleEntry把須要插入的數據放到了staleSlot處後,只是作了調用expungeStaleEntry前的準備工做,即掃描到了須要清理部分的最開始的位置,並看成參數staleSlot傳遞給了expungeStaleEntry方法,真正進行清掃工做的是expungeStaleEntry。

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
複製代碼

此方法清理了從staleSlot開始當前run內全部的staleEntry,讓指向其的引用指向null,使其變成不可達對象,以便JVM垃圾回收。從staleSlot處開始清理,當前Entry若是是staleEntry就清理掉,若是是正常的Entry,但不在根據它的key計算的本來應在的Index處,說明這個Entry當時插入的時候產生了哈希衝突,目前所在位置的Index是線性探測後找到的位置。目前所在位置前通過清掃工做後可能會整理出不少空Slot(可用位置),將當前Entry前移整理到[Index(Cal),Index(Cur) ]這個閉區間內的第一個空Slot處,這樣能夠提升下次調用get方法查找此Entry的效率。此方法的返回值是整理後的run的末尾Slot的Index。

注:Index(Cal)表明根據它的key計算的本來應在的Index,Index(Cur)表明其目前所在的根據線性探測後找到的位置Index。

未雨綢繆,再嘗試清除一些冗餘---啓發式清理cleanSomeSlots

在replaceStaleEntry方法中,expungeStaleEntry和cleanSomeSlots都是成對出現的,expungeStaleEntry會將返回的當前run末尾的slot傳遞給cleanSomeSlots,cleanSomeSlots會嘗試向後掃描logn次,若是發現了stale Entry那麼將n置爲table的長度len,作一次連續段的清理(expungeStaleEntry)(這裏n是用來進行scan control的,初始值就爲table的長度len,重置爲table的長度len,意味着循環不會退出,會繼續掃描下去,直到連續掃描(logn)+1次都沒有碰見staleEntry)。若是至少有一個stale Entry被成功清理了,那麼cleanSomeSlots就返回true不然就返回false。

private boolean cleanSomeSlots(int i, int n) {
      boolean removed = false;
      Entry[] tab = table;
      int len = tab.length;
      do {
          i = nextIndex(i, len);
          Entry e = tab[i];
          if (e != null && e.get() == null) {
              n = len;
              removed = true;
              i = expungeStaleEntry(i);
          }
      } while ( (n >>>= 1) != 0);
      return removed;
}
複製代碼

這個方法除了會在replaceStaleEntry中expungeStaleEntry清理完成後調用,也會在set方法中當一個新元素添加後調用。

static class ThreadLocalMap {
 private int size = 0;
 private int threshold;
 private void setThreshold(int len) {
    threshold = len * 2 / 3;
 }

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

 private void set(ThreadLocal<?> key, Object value) {
   ......
   tab[i] = new Entry(key, value);
   int sz = ++size;
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
                 rehash();
 }
}
複製代碼

添加新元素後,會再作一次啓發式清理cleanSomeSlots,此時若是沒有stale Entry被清理掉,而且size達到了threshold臨界值,那麼就有容量不夠的風險,rehash會再次進行清理擴容。爲何cleanSomeSlots清理成功就不須要進行sz >= threshold的判斷了呢? 首先咱們來證實一件事情, 咱們把ThreadLocalMap的set方法裏面的

int sz = ++size;
複製代碼

代碼塊記爲increment_{size},

if (!cleanSomeSlots(i, sz) && sz >= threshold)
              rehash();
複製代碼

代碼塊記爲rehash_{conditional}

如今咱們要證實,程序第n次運行完increment_{size}時,size<=threshold總成立(n爲全體正整數)。證實過程以下所示:

1.n=1時,size=0+1=1,threshold=16*2/3=10,size<=threshold成立。

2.若n=k時,程序第k次運行完increment_{size}時,size<=threshold成立。 則n=k+1時,根據set代碼的邏輯可知,第k次increment_{size}執行完成到k+1次increment_{size}執行完成之間,必定會執行一次rehash_{conditional}

Vfi0HJ.png
綜上所述,k+1時也成立

3.因此結論可證 有了這個結論後,很明顯就能得知,cleanSomeSlots執行前size<=threshold,若是cleanSomeSlots返回true那麼size必定是小於threshold的,因此就不用判斷sz >= threshold這個條件了,直接就能夠認定不須要rehash。

最後再來看看對開發者暴露的get方法

get方法比較簡單,咱們來簡單的分析一下:

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

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}

protected T initialValue() {
    return null;
}

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }

            return null;
}
複製代碼

能夠看到get方法也是取得了當前線程內的map,而後使用ThreadLocal對象做爲key查找相應的Entry,並返回Entry的value值。 總體邏輯比較簡單,有兩點須要注意一下:

  1. getEntry計算index後沒有直接找到Entry的話會進行線性探測來找具備相應key的Entry,在線性探測的過程當中若是遇見staleEntry那麼就順便調用expungeStaleEntry進行清理,而後繼續向後在當前run中查找,若是查找到了就返回Entry,不然就返回null。
  2. 若是沒有找到相應的Entry(因爲map沒初始化或者map初始化了但就是沒找到),而且是第一次調用get方法的話,即若是線程先於set(T) 方法第一次調用get方法,那麼就會調用setInitialValue方法new一個Entry並肯定一個initialValue(默認是null)放到map裏。這個方法最多會被調用一次。能夠經過爲ThreadLocal建立子類的方式重寫initialValue方法的方式改變initialValue,通常來講使用匿名內部類。以下所示:
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer> (){
    @Override
    protected Integer initialValue() {
      return new Integer(1);
    }
};
複製代碼

再思考的深刻一些

爲何線性探測過程當中,有可能具備目標key的Entry必定在當前的run裏面呢?

從ThreadLocalMap的set方法代碼中咱們能夠看出來, 其搜索可能具備目標key的Entry,範圍只是侷限在根據它的ThreadLocal對象實例計算出的Index值(即其本來應該放置的Index處)處所在的run當中。 首先咱們來思考一下,Entry數組的多個run是如何造成的? 只有兩種狀況

  1. 插入造成
    VfiggK.png
  2. 由一個run經過清除staleEntry大掃除後造成
    VfiWuD.png
    或是
    Vfi5Ed.png
    接下來咱們來分析一下,具備目標key的Entry在這兩種狀況下與根據它的ThreadLocal對象實例計算出的本來應在的Index值是否依舊保持在一個run內。 對於狀況1這種run造成過程來講,必定是具備目標key的Entry插入本來計算出的Index處的時候,發現位置已經被其餘Entry佔用了,進行線性探測找到null slot插入,這種狀況下必定是保持在一個run內的。 對於狀況2這種run造成過程來講,在大掃除以前具備目標key的Entry與本來應在的Index處必定是在一個run中的,在大掃除過程當中它們兩個之間可能會產生多個null slot,這個時候具備目標key的Entry必定會前移到離本來應在的Index處最近的null slot處(極端狀況可能就是本來應在的Index處)。這樣來看,大掃除完成後具備目標key的Entry與本來應在的Index處必定是一連串連續的不爲NULL也不是staleEntry的正常Entry,因此必定是保持在一個run內的。 綜上所述,線性探測過程當中,有可能具備目標key的Entry必定在當前的run裏面。

爲何ThreadLocal處理哈希衝突要使用線性探測法?

  1. 線性探測法採用數組實現,能夠有效地利用CPU緩存行來加速查詢速度。
  2. 線性探測法有一個很明顯的問題就是在數組的空間愈來愈滿的時候,性能會急速降低最壞甚至會到O(n),因此咱們須要隨時保持,數組有大量空閒的空間,這樣的話在大數據量下就比較浪費內存,不然性能就會不好。可是對於ThreadLocal的使用場景來講, 咱們在一個程序中並不會使用不少個ThreadLocal,數據量並不大,因此這個問題就被避免了。 綜上所述對於ThreadLocal的使用場景來講,採用線性探測法來處理哈希衝突比較適合。

聲明: 本文由CodePlus_小小的我原創,採用 CC BY 3.0 CN協議進行許可。 可自由轉載、引用,但需署名做者且註明文章出處。如轉載至微信公衆號,請在文末添加做者公衆號二維碼。

Vfmpef.jpg
相關文章
相關標籤/搜索