多線程之美2一ThreadLocal源代碼分析

目錄結構java

一、應用場景及做用
二、結構關係
   2.一、三者關係類圖
   2.二、ThreadLocalMap結構圖
   2.三、 內存引用關係
   2.四、存在內存泄漏緣由
三、源碼分析
   3.一、重要代碼片斷
   3.二、重要方法分析
   3.三、set(T): void
   3.四、get():T
   3.五、remove():void
   3.六、總結

一、應用場景及做用

-1做用、ThreadLocal 爲了實現線程之間數據隔離,每一個線程中有獨立的變量副本,操做互不干擾。區別於線程同步中,同步在爲了保證正確使用同一個共享變量,須要加鎖。
-2應用場景:
   1)能夠對一次請求過程,作一個過程日誌追蹤。如slf4j的MDC組件的使用,能夠在日誌中每次請求過程加key,方便定位一次請求流程問題。 
   2)解決線程中全局數據傳值問題。

二、結構關係

要理清ThreadLocal的原理做用,能夠先了解Thread, ThreadLocal, ThreadLocalMap三者之間的關係。簡單類圖關係以下算法

2.一、三者關係類圖

一、Thread 類中有ThreadLocalMap類型的成員變量 threadLocals
二、ThreadLocalMap是ThreadLocal的靜態內部類 
三、Thread 與 ThreadLocal怎麼關聯?
   線程對象中threadLocals中存儲的鍵值對 key--> ThreadLocal對象,value --> 線程須要保存的變量值

2.二、ThreadLocalMap結構圖

ThreadLocalMap 底層實現實質是一個Entry對象數組, 默認容量是16,在存儲元素到數組中,本身實現了一個算法來尋址(計算數組下標), 與Map集合中的HashMap有所不一樣。 Entry對象中 key是ThreadLocal對象。
誤區:在不瞭解原理前,會想線程之間要實現數據隔離,那這個集合中key應該是Thread對象,這樣在存的時候,以當前線程對象爲key,value爲要保存的值,這樣在獲取的時候,經過線程對象去get獲取相應的值。

2.三、 內存引用關係

-1,同一個ThreadLocal對象可被多個線程引用,每一個線程之間本地變量副本存儲,實現數據獨立性,可見每一個線程內部都有單獨的map集合,即便引用的ThreadLocal同一個,value能夠不一樣,如圖中ThreadLocal1對象,同時被線程A,B引用做爲key
-2,一個線程能夠存儲多個ThreadLocal,因線程中存儲的只能存儲同一個ThreadLocal對象一次,再次存儲相同的Threadlocal對象,由於key相同,會覆蓋原來的value,value能夠是基本數據類型的值,也能夠是引用數據類型(如封裝的對象)

2.四、存在內存泄漏緣由

ThreadLocal對象沒有外部強引用後,只存在弱引用,下一次GC會被回收。以下:數組

-1,上圖實線箭頭表明強引用,虛線表明弱引用; JVM存在四種引用:強引用,軟引用,弱引用,虛引用,弱引用對象,會在下一次GC(垃圾回收)被回收。
-2,上圖可見Entry的 Key指向的ThreadLocal對象的引用是弱引用,一旦tl的強引用斷開,沒有外部的強引用後,在下一次JVM垃圾回收時,ThreadLocal對象被回收了,此時 key--> null,而此時 Entry對象,是有一條強引用鏈的,th-->
 Thread對象-->ThreadLocalMap--> Entry,可達性性分析是可達的,這時ThreadLocalMap集合,即在數組的某一個索引是有Entry引用的,可是該Entry的key爲null,value依然有值,但再也用不了了,這時的Entry稱爲staleEntry(我理解爲失效的Entry),形成內存泄漏。
-3,內存泄漏是指分配的內存,gc回收不了,本身也用不了; 內存溢出,是指內存不夠,若有剩餘2M內存,這時有一個對象建立須要3M,內存不夠,致使溢出。內存泄漏可能會致使內存溢出,由於內存泄漏就會有人佔着茅坑不拉屎,可用空間愈來愈少,gc也回收不了,最終致使內存溢出。
-4,那線程對象被回收了,這條引用鏈斷了就沒事了,下次Gc就會把ThreadLocalMap集合中對象所有回收了,就不存在內存泄漏問題了;但開發環境,線程通常會在線程池建立來節約資源,每一個線程是被重複使用的,生命週期很長,線程對象長時間是存在內存中的,而ThreadLocalMap和Thread生命週期相同,只有線程結束,它內部持有的ThreadLocalMap對象纔會銷燬,以下Thread#exit:
  private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        //線程退出時,才斷開ThreadLocalMap引用
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

三、源碼分析

本次源碼分析,主要分析ThreadLocal的set,get,remove三個方法,分別以此爲入口,一步步深刻每一個代碼方法查看其實現原理,在這分析以前,先撿幾個我理解比較重要的方法或者代碼片斷先解釋一下,有一個初步的理解,後面會更順暢。源碼分析

3.一、重要代碼片斷

//1、ThreadLocalMap的尋址,因其底層是數組,在存放元素如何定位索引i存儲? 
   //兩個要求:1)求的索引位置必定要在數組大小內
     //       2)索引足夠均分分散,要求hashcode足夠散列,目的減小hash衝突。
            //firstKey.threadLocalHashCode,就是爲了達到要求2,均分分散
            // &(INITIAL_CAPACITY - 1) 爲了落在數組範圍內,經常使用進行模運算,這裏是巧妙運用位運算,效率更高, %2^n與 &(2^n-1)等價,因此要求數組的容量要爲2的冪;
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);


// -------------------> firstKey.threadLocalHashCode,
//傳入的ThreadLocal對象,作了 0x61c88647的增量後求得hash值,爲何要加0x61c88647呢,與斐波那契數列有關,反正是一個神奇的魔法值,目的就是使的hash值更分散,減小hash衝突。
 private final int threadLocalHashCode = nextHashCode();
 private static final int HASH_INCREMENT = 0x61c88647;
 private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }


//2、如何ThreadLocalMap中,出現hash衝突了,即2個ThreadLocal對象的hash計算出來是相同的下標,這裏解決hash衝突使用線性探測法,即這個位置衝突,就尋找下一個位置,若是到數組終點了呢,從0再開始,因此這裏數組邏輯上是一個首尾相接的環形數組。

//1,向後遍歷,獲取索引位置
private static int nextIndex(int i, int len) {
     return ((i + 1 < len) ? i + 1 : 0);
}
//2,向前遍歷
private static int prevIndex(int i, int len) {
      return ((i - 1 >= 0) ? i - 1 : len - 1);
}
以下圖:

3.二、重要方法分析

cleanSomeSlots 原理圖1以下:this

//一,分析清理失效Entry方法,清理起始位置是staleSlot
//已經知道某個Entry的key==null了,那麼數組該位置的引用應該被清除 
   private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

         //Entry的value引用也清除,方便gc回收  
            tab[staleSlot].value = null;
         // 清理數組當前位置的Entry
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
           //向後循環遍歷,直到遇到 null
           //作2件事:
               //1)遇到其餘失效Entry,順手清除
               //2)沒有失效的Entry,從新hash一下,安排新位置;由於可能以前某些位置有hash衝突,致使根據key生成hash的值與當前的位置i不一致(衝突,會日後順延,這裏是邏輯上日後,達到數組長度,從0開始),而這時又清理了很多失效的Entry,可能會有空位了,因此從新hash調一下順序,提升效率。
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                  //1,失效Entry,清除
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                   //2,hash值與當前數組索引位置不一樣
                    if (h != i) {
                        tab[i] = null;
                       //3,向後遍歷,找合適空位置插入
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
          //返回i位置, Entry ==null
            return i;
        }


//2、可伸縮性遍歷某段範圍失效的Entry cleanSomeSlots(int i, int n),原理如上圖1
//爲何要有伸縮性,我理解仍是爲了效率,若是發現這範圍內有須要清理的失效Entry,才把範圍放大一些查找清除,源代碼以下:

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];
               // 碰見有失效的Entry,當n傳入的是size,即數組實際容納的數量,n擴大到數組長度了,
              //影響在於,原來清理遍歷的只是數組的一個小範圍,一會兒擴大到了整個數組。我理解這樣作爲了提升執行效率,沒有檢測到失效entry就小範圍清理一下,檢測到就大範圍清理。
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            // n >>>= 1,即 n向右位移1位,即 n/2, 可循環次數log2n次
            } while ( (n >>>= 1) != 0);
            return removed;
        }

//3、當在set時,發現當前生成的數組位置已經被其餘Entry佔了,可是它失效了,key==null,這時須要把它給替換了吧,replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot),較難理解,多看幾遍哈,原理圖看下面,分析了2種狀況,還有先後遍歷都發現有失效的Entry狀況,請自行腦補了哈。

//源代碼以下:

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            // 1,要清除的位置
            int slotToExpunge = staleSlot;
           //2,從i 向前遍歷,找到左邊第一個失效的位置(指的是Entry !=null,key==null)
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;
            //3,從i向後遍歷
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
              
                //4,傳入 staleSlot的 key==null,是一個失效的Entry, 從staleSlot+1個向後遍歷,若是
              //碰見 k==key,將staleSlot索引位置與此處i替換位置,即將失效的Entry日後面放
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //若是相等,staleSlot左邊沒有失效的entry,賦值爲此處i,此處已經替換爲失效Entry了,若是不相等,那麼就清除失效Entry,以staleSlot最左邊那一個失效entry開始清除
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                   //cleanSomeSlots的目的:expungeStaleEntry返回的是entry ==null的索引i,
                  //清理i到len這一段的失效entry,中間會有null的狀況嗎?
                    return;
                }
               //5,slotToExpunge == staleSlot 表示 左邊沒有失效entry, 右邊碰見第一失效entry,標記此處索引,以便後文肯定從哪裏開始清除無效entry
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            //解除value的引用,gc會回收
            tab[staleSlot].value = null;
            //數組失效Entry位置,賦值新的Entry
            tab[staleSlot] = new Entry(key, value);

           //6,不相等,確定有失效索引須要清理,執行清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

replaceStaleEntry 情景圖1以下:線程

replaceStaleEntry 情景圖2以下:debug

3.三、set(T): void

set 方法,是代碼最多的,也是最重要的。其中expungeStaleEntry, replaceStaleEntry,cleanSomeSlots 三個方法較爲主要,目的是找出、清理失效的Entry的過程,其中replaceStaleEntry 較難理解。設計

//1、從 set() 着手,入口

public void set(T value) {
        //1,獲取當前線程對象
        Thread t = Thread.currentThread();
        //2,獲取當前線程的map, 每一個線程持有一個threadLocals對象,經過該map來實現線程之間數據的隔離,達到每一個線程擁有本身獨立的局部變量。 見代碼分析二
        ThreadLocalMap map = getMap(t);
        if (map != null)
           // 見代碼分析四
            map.set(this, value);
        else 
          //3,若是當前線程持有的map爲空,建立map,見代碼分析三 
            createMap(t, value);
    }
 

//2、 獲取ThreadLocalMap 方法,獲取當前線程持有的map對象 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

 // Thread類中持有hreadLocalMap類型的對象,該map是ThreadLocal的靜態內部類
    ThreadLocal.ThreadLocalMap threadLocals = null;


//3、代碼分析 
   void createMap(Thread t, T firstValue) {
       // 建立 map對象,下見ThreadLocalMap的構造方法,很關鍵,該map與經常使用的HashMap等不一樣
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
   
   //構造方法,ThreadLocalMap 可以實現key-value的map集合結構,底層實際是一個數組,Entry爲其每一個節點對象,Entry 包含key和value
   ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
           //1,初始化容量爲16的Entry[]數組 
            table = new Entry[INITIAL_CAPACITY];
           //2,這一步目的就是根據傳入的ThreadLocal對象做爲key,爲了求放在數組下的索引位置,肯定放在哪
           //兩個要求:1)求的索引位置必定要在數組大小內(這裏即0-15範圍)
             //      2)索引足夠均分分散,要求hashcode足夠散列,目的減小hash衝突。
            //firstKey.threadLocalHashCode,就是爲了達到要求2
            // &(INITIAL_CAPACITY - 1) 爲了落在數組範圍內,經常使用進行模運算,這裏是巧妙運用位運算,效率更高, %2^n與 &(2^n-1)等價
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
           //求得索引位置,放入數組中
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
           //設置數組容量閾值,即填充因子,用於後續判斷是否須要擴容
            setThreshold(INITIAL_CAPACITY);
        }



 //爲了使傳入的ThreadLocal對象求在數組索引位置,求的其hashcode,加上了0x61c88647增量,目的是爲了足夠分散
  private static final int HASH_INCREMENT = 0x61c88647;
    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

     
  //設置數組擴容閾值
        private void setThreshold(int len) {
           //初始填充因子爲2/3,數組容量的2/3,即 16*2/3=10
            threshold = len * 2 / 3;
        }


//4、代碼分析 set(key,value)
     private void set(ThreadLocal<?> key, Object value) {
       
            Entry[] tab = table;
            int len = tab.length;
            //求數組索引位置
            int i = key.threadLocalHashCode & (len-1);
            //這裏用for循環,是爲了解決hash衝突時,查找下一個可用 slot(卡槽,位置; 即生成的索引i,發現已有Entry佔用了,找下一個位置插入,這裏解決hash衝突方式不一樣於hashmap的拉鍊法(在衝突位置,以鏈表形式串接),這裏採用的是線性尋址法,即數組當前i位置被佔用了,看第i+1個位置,若是i+1已經大於等於數組length,再從數組下標0 從頭開始,從該i = nextIndex(i, len)可知道是邏輯上這是一個首尾循環式數組)
       
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
      //1,從i向後遍歷,若Entry爲null,跳出循環; 不爲null,獲取Entry的key,線程初始化 threadLocalMap集合就有3個 Entry(此處不解?debug看了)
                ThreadLocal<?> k = e.get();
                //2,若是數組當前位置key與將要設值的 threadlocal對象相等,覆蓋原value,返回 
                if (k == key) {
                    e.value = value;
                    return;
                }
            
                if (k == null) {
          //3,若是數組當前位置key爲空,須要替換失效的Entry(stale:不新鮮的,Entry的key ==null)
                //見代碼分析五 
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //4,若是上面for循環,出現hash衝突了,跳出循環,此時索引i位置 Entry==null,在此插入新Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
           //5,cleanSomeSlots,順便清理一下失效的Entry,避免內存泄漏,見代碼分析六
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
               // 6,清理失敗且當前數組的Entry數量達到設定閾值了,執行 rehash,見代碼分析八
                rehash();
        }

//5、 分析 replaceStaleEntry(key, value, i) 替換失效的Entry

  private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            // 1,要清除的位置
            int slotToExpunge = staleSlot;
           //2,從i 向前遍歷,找到左邊第一個失效的位置(指的是Entry !=null,key==null)
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;
            //3,從i向後遍歷
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
              
                //4,傳入 staleSlot的 key==null,是一個失效的Entry, 從staleSlot+1個向後遍歷,若是
              //碰見 k==key,將staleSlot索引位置與此處i替換位置,即將失效的Entry日後面放,
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //若是相等,staleSlot左邊沒有失效的entry,賦值爲此處i,此處已經替換爲失效Entry了,若是不相等,那麼就清除失效Entry,以staleSlot最左邊那一個失效entry開始清除
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                   //cleanSomeSlots的目的:expungeStaleEntry返回的是entry ==null的索引i,
                  //清理i到len這一段的失效entry,中間會有null的狀況嗎?
                    return;
                }
               //5,slotToExpunge == staleSlot 表示 左邊沒有失效entry, 右邊碰見第一失效entry,標記此處索引,以便後文肯定從哪裏開始清除無效entry
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            //解除value的引用,gc會回收
            tab[staleSlot].value = null;
            //數組失效Entry位置,賦值新的Entry
            tab[staleSlot] = new Entry(key, value);

           //6,不相等,確定有失效索引須要清理,執行清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }


//6、分析 cleanSomeSlots(int i, int n),清理某些失效的Entry方法
//i爲 失效位置, n分2種傳入場景  
// 1)數組的實際Entry數量 size 
// 2) 數組的容量 length
 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];
               // 碰見有失效的Entry,當n傳入的是size,即數組實際容納的數量,n擴大到數組長度了,
              //影響在於,原來清理遍歷的只是數組的一個小範圍,一會兒擴大到了整個數組。我理解這樣作爲了提升執行效率,沒有檢測到失效entry就小範圍清理一下,檢測到就大範圍清理。
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            // n >>>= 1,即 n向右位移1位,即 n/2, 可循環次數log2n次
            } while ( (n >>>= 1) != 0);
            return removed;
        }



//7、分析清理失效Entry方法,清理起始位置是staleSlot
   private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 清理當前位置的Entry
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
           //向後循環遍歷,直到遇到 null
           //作2件事:
               //1)遇到其餘失效Entry,順手清除
               //2)沒有失效的Entry,從新hash一下,安排新位置;由於可能以前某些位置有hash衝突,致使根據key生成hash的值與當前的位置i不一致(衝突,會日後順延,這裏是邏輯上日後,達到數組長度,從0開始),而這時又清理了很多失效的Entry,可能會有空位了,因此從新hash調一下順序,提升效率。
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                  //1,失效Entry,清除
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                   //2,hash值與當前數組索引位置不一樣
                    if (h != i) {
                        tab[i] = null;
                       //3,向後遍歷,找合適空位置插入
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
          //返回i位置, Entry ==null
            return i;
        }

//8、rehash
   //首先掃描全表,清除全部失效的Entry, 若是這還不能充分地縮小數組的大小,擴容爲當前的2倍
        private void rehash() {
           //1,清除全部失效的entry,見代碼分析九
            expungeStaleEntries();

            //2,threshold = length * 2/ 3
            //size >= threshold - threshold / 4 = threshold*3/4 ,
            //即size >= length *2/3 *3/4= length* 1/2, 只要數組的大小>=於數組容量的一半,就擴容。
            if (size >= threshold - threshold / 4)
               //見代碼分析十
                resize();
        }


//9、遍歷數組所有節點,清除失效的Entry
 private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }

//10、擴容爲原來的2倍
  private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;
          // 舊數組數據向新數組遷移,順便清除失效的entry的value,幫助Gc容易發現它,直接回收
          //Entry不清除了嗎?這裏舊數組以後就沒有被人引用了,下次Gc會直接回收
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
    
           setThreshold(newLen);
            size = count;
            table = newTab;
    
  }

3.四、get():T

//1、從get() 着手
   public T get() {
       //1,獲取當前線程對象
        Thread t = Thread.currentThread();
       //2,獲取該線程的 map集合,每一個線程都有單獨的map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
           //3,this指的是 ThreadLocal對象,以它爲key,去map中獲取相應的Entry,
          //易混淆:ThreadLocalMap 中存儲的Entry鍵值對,key是ThreadLocal對象,而不是線程對象。
          //此處 map.getEntry(this) 下面代碼二 分析
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
               //4,返回value
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
       //該線程若沒有map對象, 返回初始默認值,詳見代碼分析四
        return setInitialValue();
    }


// 2、分析 map.getEntry(this)

  private Entry getEntry(ThreadLocal<?> key) {
            //1,獲取數組索引位置
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
             //2,直接就命中,沒有hash衝突,返回
                return e;
            else
              //3,遍歷其餘Entry,見代碼分析三
                return getEntryAfterMiss(key, i, e);
        }


//3、根據key獲取Entry,沒有直接命中,繼續遍歷查找

 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) 
                 //1,命中返回,爲啥重複判斷一次? 由於這是在while循環,會日後執行再判斷
                    return e;
                if (k == null)
                  //2,當前位置Entry失效,清除
                    expungeStaleEntry(i);
                else
                  //3,hash衝突,獲取下一個索引
                    i = nextIndex(i, len);
                e = tab[i];
            }
          //4,數組中沒有找到該key
            return null;
        }

//4、沒有map,返回默認值,初始化操做

    private T setInitialValue() {
       //1,調用默認的初始化方法, 以下,通常用來被重寫的,給定一個初始值
        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;
    }

3.五、remove():void

// 1、入口  
public void remove() {
        //1,獲取當前線程的map集合 
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
           //2,見代碼分析二
             m.remove(this);
     }


// 2、 m.remove(this);
   private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //一、獲取該key在數組中索引位置 
            int i = key.threadLocalHashCode & (len-1);
           //2,從i位置向後循環判斷,考慮hash衝突
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //3,找到該key,
                if (e.get() == key) {
                   //4,引用置空 
                    e.clear();
                   // 5,從i開始清除失效的Entry,避免內存泄漏
                    expungeStaleEntry(i);
                    return;
                }
            }
  }

//引用置空
 public void clear() {
        this.referent = null;
    }

3.六、總結

從set,get,remove代碼可見,每一個方法都會去清除失效的Entry,說明設計者也考慮到內存泄漏的問題,因此建議在使用完ThreadLocal,及時執行remove方法清除一下,避免潛在的內存泄漏問題。
相關文章
相關標籤/搜索