ThreadLocal原理分析

bmilk發表於2020-05-26

本文結構

  • ThreadLocal簡介 (簡要說明ThreadLocal的作用)
  • ThreadLocal實現原理(說明ThreadLocal的常用方法和原理)
  • ThreadLocalMap的實現 (說明核心資料結構ThreadLocalMap的實現)

ThreadLocal簡介


先貼一段官方的文件解釋

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

大意是ThreadLocal類提供了一個執行緒的本地變數,每一個執行緒持有一個這個變數的副本並且每個執行緒讀取get()到的值是不一樣的,可以通過set()方法設定這個值;
在某些情況下使用ThreadLocal可以避免共享資源的競爭,同時與不影響執行緒的隔離性

通過threadLocal.set方法將物件例項儲存在每個執行緒自己所擁有的threadLocalMap中,
這樣每個執行緒使用自己儲存的ThreadLocalMap物件,不會影響執行緒之間的隔離。

看到這裡的第一眼我一直以為ThreadLocal是一個map,每一個執行緒都是一個key,對應一個value,但是是不正確的。正確的是每個執行緒持有一個ThreadLocalMap的副本,這個map的鍵是ThreadLocal物件,各個執行緒中同一key對應的值可以不一樣。

ThreadLocal實現原理


ThreadLocal中的欄位與構造方法

詳細說明參考註釋

public class ThreadLocal<T> {
  
   //當前ThreadLocal物件的HashCode值,
   //通過這個值可以定位Entry物件在ThreadLocalMap中的位置
   //由nextHashCode計算得出
   private final int threadLocalHashCode = nextHashCode();

   //一個自動更新的AtomicInteger值,官方解釋是會自動更新,怎麼更新的不知道,
   //看完AtomicInteger原始碼回來填坑
   private static AtomicInteger nextHashCode =
       new AtomicInteger();

   //ThreadLocal的魔數
   //0x61c88647是斐波那契雜湊乘數,它的優點是通過它雜湊(hash)出來的結果分佈會比較均勻,可以很大程度上避免hash衝突,
   private static final int HASH_INCREMENT = 0x61c88647;

   private static int nextHashCode() {
       //原子操作:將給定的兩個值相加
       return nextHashCode.getAndAdd(HASH_INCREMENT);
   }

   /**
    * 返回當前執行緒變數的初始值
    * 這個方法僅在沒有呼叫set方法的時候第一次呼叫get方法呼叫
    */
   protected T initialValue() {
       return null;
   }
   //構造方法
   public ThreadLocal() {
   }
  
   //建立ThreadLocalMap,
   //當前的ThreadLocal物件和value加入map當中
   //賦值給當前執行緒的threadLocals欄位
   void createMap(Thread t, T firstValue) {
       t.threadLocals = new ThreadLocalMap(this, firstValue);
   }
.......
}

常用方法get()、set()、remove

get()方法

get()方法的原始碼,具體的程式碼解釋請看註釋

//返回當前執行緒中儲存的與當前ThreadLocal相關的執行緒變數的值(有點繞,可以看程式碼註釋)
public T get() {
   Thread t = Thread.currentThread();
   
   //返回當前執行緒的threadLocals欄位的值,型別是ThreadLocalMap
   //暫時可以將ThreadLocalMap當作HashMap,下文解釋
   ThreadLocalMap map = getMap(t);
   if (map != null) {

       //Entry也可以按照HashMap的entry理解
       //Entry儲存了兩個值,一個值是key,一個值是value
       //返回當前ThreadLocalMap中當前ThreadLcoal對應的Entry
       ThreadLocalMap.Entry e = map.getEntry(this);
       if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;

           //返回Entry中對應的值
           return result;
       }
   }
   //如果當前執行緒的ThreadLocalMap不存在,則構造一個
   return setInitialValue();
}

getMap(Thread t) 方法

//返回當前執行緒的threadLocals欄位的值,型別為ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
       return t.threadLocals;
   }

getEntry(ThreadLocal<?> key)方法

//table是一個陣列,具體的可以看下文的ThreadLocalMap解釋
//返回當前ThreadLocalMap中key對應的Entry

private Entry getEntry(ThreadLocal<?> key) {
   //根據key值計算所屬Entry所在的索引位置(同HashMap)
   
   int i = key.threadLocalHashCode & (table.length - 1);
   Entry e = table[i];
   //由於存在雜湊衝突,判斷當前節點是否是對應的key的節點
   if (e != null && e.get() == key)
       //返回這個節點
       return e;
   else
       return getEntryAfterMiss(key, i, e);
}

setInitialValue()方法

 private T setInitialValue() {
     
     //在構造方法部分有寫,返回一個初始值,
     //預設情況(沒有被子類重寫)下是一個null值
     T value = initialValue();
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     //再次判斷當前執行緒中threadLocals欄位是否為空值
     if (map != null)
         //set可以看作HashMap中的put
         map.set(this, value);
     else
         //當map為null時
         //構造一個ThreadLocalMap,
         //並以自身為鍵,initialValue()的結果為值插入ThreadLocalMap
         //並賦值給當前執行緒的threadLocals欄位
         createMap(t, value);
     return value;
 }

createMap方法

void createMap(Thread t, T firstValue) {
 //呼叫THreadLocalMap的構造方法
 //構造一個ThreadLocalMap,
 //使用給定的值構造一個儲存的例項(Entry的物件)儲存到map中
 //並儲存到thread的threadLocals欄位中
 t.threadLocals = new ThreadLocalMap(this, firstValue);
}

        從get()方法中大概可以看出ThreadLocalMap是一個以ThreadLocal物件為鍵一個Map,並且這個ThreadLocalMap物件由Thread類維護,並儲存在threadLocals欄位中,不同的ThreadLocal物件可以以自身為鍵訪問這個Map中對應位置的值。

        當第一次呼叫get()(之前沒有呼叫過set()或者呼叫了remove())時,會調`initialValue()新增當前ThreadLocal物件對應的值為null並返回。

set()方法

set()方法的原始碼,具體的程式碼的解釋請看註釋

/**
* 設定當前執行緒的執行緒變數的副本為指定值
* 子類一般不用重寫這個方法
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
*        this thread-local.
*/
public void set(T value) {
   Thread t = Thread.currentThread();
   //返回當前執行緒的threadLocals欄位的值,型別是ThreadLocalMap,同get()方法
   ThreadLocalMap map = getMap(t);
   //同get()一樣 判斷當前執行緒的threadLocals欄位的是否為null
   if (map != null)
       //不為null,設定當前ThreadLocal(key)對應的值(value)為指定的value
       map.set(this, value);
   else
       //null,建立ThreadLocalMap物件,將[t, value]加入map,並賦值給當前執行緒的localThreads欄位
       createMap(t, value);
}

remove()方法

remove()方法的原始碼,具體程式碼的解釋請看註釋

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
          //從當前執行緒的threadLocals欄位中移除當前ThreadLocal物件為鍵的鍵值對
          //remove方法在 ThreadLocalMap中實現
          m.remove(this);
     }

從上面的實現程式碼看,get()、set()、remove這三個方法都是很簡單的從一個ThreadLocalMap中獲取設定或者移除值,那麼有一個核心就是ThreadLocalMap,那麼下面就分析下ThreadLocalMap

ThreadLocalMap的實現


個人覺得replaceStaleEntry()expungeStaleEntry()cleanSomeSlots()這三個方法是ThreadLocal中非常重要難以理解的方法;

/**
 * ThreadLocalMap 是一個定製的雜湊雜湊對映,僅僅用用來維護執行緒本地變數
 * 對其的所有操作都在ThreadLocal類裡面。
 * 使用軟引用作為這個雜湊表的key值(軟引用引用的物件在強引用解除引用後的下一次GC會被釋放)
 * 由於不使用引用佇列,表裡的資料只有在表空間不足時才會被釋放
 * (因為使用的時key-value,在key被釋放·null·後這個表對應的位置不會變為null,需要手動釋放)
 *  這個map和HashMap不同的地方是,
 *  在發生雜湊衝突的時候HashMap會使用連結串列(jdk8之後也可能是紅黑樹)儲存(拉鍊法)
 *  而這裡使用的是向後索引為null的表項來儲存(開放地址法)
 */
static class ThreadLocalMap {

    /**
     * 這個hash Map的條目繼承了 WeakReference, 使用他的ref欄位作為key(一個ThreadLocal物件)
     * ThreadLocal object).  注意當鍵值為null時代表整個鍵已經不再被引用(ThreadLocal
     * 物件已經被垃圾回收)因此可以刪除對應的條目
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            //key值是當前TreadLocal物件的弱引用
            super(k);
            value = v;
        }
    }

    //類的屬性欄位,基本和HashMap作用一致
    
    //初始容量,必須是2的冪
    private static final int INITIAL_CAPACITY = 16;

    //雜湊表 
    //長度必須是2的冪,必要時會調整大小
    private Entry[] table;

    //表中儲存資料的個數
    private int size = 0;

    //當表中資料的個數達到這個值時需要擴容
    private int threshold; // Default to 0

    //設定threshold ,負載係數時2/3,也就是說當前表中資料的條目
    //達到表總容量的2/3就需要擴容
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    /**
     * Increment i modulo len.
     */
    //這個註釋不明白,但是在程式碼實現中遍歷表的的時候用來判斷是否到了表的結尾
    //如果到了表節位就從表首接著這遍歷
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
     * Decrement i modulo len.
     */
    //這個註釋不明白,但是在程式碼實現中遍歷表的的時候用來判斷是否到了表的頭部
    //如果到了表節位就從表尾接著這遍歷
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

    //構造一個新map包含 (firstKey, firstValue).
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //構造表
        table = new Entry[INITIAL_CAPACITY];
        //確定需要加入的資料的位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        構造一個新的Entry物件並放入到表的對應位置
        table[i] = new Entry(firstKey, firstValue);
        //設定當前表中的資料個數
        size = 1;
        // 設定需要擴容的臨界點
        setThreshold(INITIAL_CAPACITY);
    }

    //這個方法在建立一個新執行緒呼叫到Thread.init()方法是會被呼叫
    //目的是將父執行緒的inheritableThreadLocals傳遞給子執行緒
    //建立的map會被儲存在Thread.inheritableThreadLocals中
    //根據parentMap構造一個新的ThreadLocalMap,
    //這個map包含了所有parentMap的值
    //只有在createInheritedMap呼叫
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        setThreshold(len);
        //根據parentMap的長度(容量)構造table
        table = new Entry[len];

        //依次複製parentMap中的資料
        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {

                    //childValue在InheritableThreadLocal中實現
                    //也只有InheritableThreadLocal物件會呼叫這個方法
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);

                    //解決雜湊衝突的辦法是向後索引
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }

getEntry()獲取指定key對應的Entry物件

    /**
     * 通過key獲取key-value對.  這個方法本身只處理快速路徑(直接命中)
     * 如果沒有命中繼續前進(getEntryAfterMiss)
     * 這是為了使命中效能最大化設計
     */
    private Entry getEntry(ThreadLocal<?> key) {
        //計算key應該出現在表中的位置
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        //判斷獲取到的entry的key是否是給出的key
        if (e != null && e.get() == key)
            //是就返回entry
            return e;
        else
            //否則向後查詢,
            return getEntryAfterMiss(key, i, e);
    }

    /**
     * getEntry的派生,當直接雜湊的槽裡找不到鍵的時候使用
     *
     * @param  key the thread local object
     * @param  i key在table中雜湊的結果
     * @param  e the entry at table[i]
     * @return the entry associated with key, or null if no such
     */
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        
        //根絕ThreadLocalMapc插入的方法,插入時通過雜湊計算出來的槽位不為null
        //則向後索引,找到一個空位放置需要插入的值
        //所以從雜湊計算的槽位到插入值的位置中間一定是不為null的
        //因為e!=null可以作為迴圈終止條件
        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                //如果命中則返回
                return e;
            if (k == null)
                //e!=null && k==null,證明對應的ThreadLcoal物件已經被釋放
                //那麼這個位置的entry就可以被釋放
                //釋放位置i上的空間
                //釋放空間也是ThreadLocalMap與HashMap不相同的地方
                expungeStaleEntry(i);
            else
                //獲取下一個查詢的表的索引下標
                //當i>=len時會從0號位重新開始查詢
                i = nextIndex(i, len);
            e = tab[i];
        }
        //沒找到返回null
        return null;
    }

set()修改或者建立指定的key對應的Entry物件


    //新增一個key-value對
    private void set(ThreadLocal<?> key, Object value) {

        // 不像get一樣使用快速路徑,
        // set建立新條目和修改現有條目一樣常見
        // 這種情況下快速路徑通常會失敗

        Entry[] tab = table;
        int len = tab.length;
        //計算應該插入的槽的位置
        int i = key.threadLocalHashCode & (len-1);

        //雜湊計算的槽位到插入值的位置中間一定是不為null的
        //該位置是否位null可以作為迴圈終止條件
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            
            ThreadLocal<?> k = e.get();

            //修改現有的鍵值對的值
            if (k == key) {
                e.value = value;
                return;
            }
            
            //e!=null && k==null,證明對應的ThreadLcoal物件已經被釋放
            //那麼這個位置的entry就可以被釋放
            //釋放位置i上的空間
            //釋放空間也是ThreadLocalMap與HashMap不相同的地方
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        
        //在可以插入值的地方插入
        tab[i] = new Entry(key, value);
        int sz = ++size;
        //清除部分k是null的槽然後判斷是否需要擴容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            //擴容
            rehash();
    }

remove()移除指定key對應的Entry物件

    /**
     * Remove the entry for key.
     */
    private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        //同set()
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                //弱引用引用置空,標記這個槽已經是舊槽
                e.clear();
                //清理舊槽
                expungeStaleEntry(i);
                return;
            }
        }
    }

set()過程中處理舊槽的核心方法——replaceStaleEntry()


    /**
     * Replace a stale entry encountered during a set operation
     * with an entry for the specified key.  The value passed in
     * the value parameter is stored in the entry, whether or not
     * an entry already exists for the specified key.
     *
     * As a side effect, this method expunges all stale entries in the
     * "run" containing the stale entry.  (A run is a sequence of entries
     * between two null slots.)
     *
     * @param  key the key
     * @param  value the value to be associated with key
     * @param  staleSlot index of the first stale entry encountered while
     *         searching for key.
     */
    //run:兩個空槽中間所有的非空槽
    //姑且翻譯成執行區間
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;

        // 備份檢查當前執行中的以前的陳舊條目.
        // 我們每次清理整個執行區間,避免垃圾收集器一次釋放過多的引用
        // 而導致增量的雜湊


        // slotToExpunge刪除的槽的起始位置,因為在後面清除(expungeStaleEntry)
        // 的過程中會掃描從當前位置到第一個空槽之間的位置,所以這裡只需要判斷出
        // 掃描的開始位置就可以
        int slotToExpunge = staleSlot;
        
        //向前掃描找到最前面的舊槽
        for (int i = prevIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = prevIndex(i, len))
            //在向前掃描的過程中找到了舊槽舊覆蓋舊槽的位置
            if (e.get() == null)
                slotToExpunge = i;

        // Find either the key or trailing null slot of run, whichever
        // occurs first
        //從傳進來的舊槽的位置往後查詢key值或者第一個空槽結束
        for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

            // 如果在staleSlot位置的槽後面找到了指定的key,那麼將他和staleSlot位置的槽進行交換
            // 以保持雜湊表的順序
            // 然後將新的舊槽護著上面遇到的任何過期的槽通過expungeStaleEntry刪除
            // 或者重新雜湊所有執行區間的其他條目

            //找到了對應的key,將他和staleSlot位置的槽進行交換
            if (k == key) {
                e.value = value;

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

                // 判斷清理舊槽的開始位置
                // 如果在將staleSlot之前沒有舊槽,那麼就從當前位置為起點清理
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                
                //expungeStaleEntry清理從給定位置開始到第一個null的區間的空槽
                //並返回第一個null槽的位置p
                //cleanSomeSlots從expungeStaleEntry返回的位置p開始清理log(len)次表

                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

                //將給定的key-value已經存放,也清理了相應執行區間 ,返回
                return;
            }

            // 如果在向後查詢後沒有找到對應的key值,而當前的槽是舊槽
            // 同時如果在向前查詢中也沒查詢到舊槽
            // 那麼進行槽清理的開始位置就是當前位置
            //為什麼不是staleSlot呢?因為在這個位置建立指定的key-value存放
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 如果在向後查詢後沒有找到對應的key值,在staleSlot位置建立該key-value
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 確定掃描過程中發現過空槽
        if (slotToExpunge != staleSlot)
            //清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

replaceStaleEntry()計算了很久的掃描清理的起點位置,總結下應該分為四種情況:

  • prev方向上沒有舊槽,在next方向上找到key值之前沒有找到舊槽,那麼就交換keystaleSlot然後從當前位置向後清理空槽
  • prev方向上沒有舊槽,在next方向上沒有找到key沒有找到舊槽,那麼在staleSlot位置建立指定的key-value
  • prev方向上沒有舊槽,在next方向上沒有找到key但是找到舊槽,那麼在staleSlot位置建立指定的key-value,並從找到的第一個舊槽的位置開始清理舊槽
  • prev方向上找到舊槽,在next方向上沒有找到key,那麼在staleSlot位置建立指定的key-value,從prev方向最後一個找到的舊槽開始清理舊槽
  • prev方向上找到舊槽,在next方向上找到key,那麼就交換keystaleSlot,從prev方向最後一個找到的舊槽開始清理舊槽

求推薦個好看的畫圖工具

清理舊槽的核心方法——expungeStaleEntry()cleanSomeSlots()

    /**
     * 刪除指定位置上的舊條目,並掃描從當前位置開始到第一個發現的空位之間的陳舊條目
     * 還會刪除尾隨的第一個null之前的所有舊條目
     */
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // 釋放槽
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        // 重新雜湊直到遇到null
        Entry e;
        int i;
        //第staleSlot槽已經空出來,
        //從下一個槽開始掃描舊條目直到遇到空槽
        //因為整個表只適用2/3的空間,所以必然會遇到空槽
        //刪除掃描期間遇到的空槽
        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;

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    //尋找到從第h位開始的第一個空槽放置
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        //返回掃描過程中遇到的第一個空槽
        return i;
    }

    /**
     * Heuristically scan some cells looking for stale entries.
     * This is invoked when either a new element is added, or
     * another stale one has been expunged. It performs a
     * logarithmic number of scans, as a balance between no
     * scanning (fast but retains garbage) and a number of scans
     * proportional to number of elements, that would find all
     * garbage but would cause some insertions to take O(n) time.
     *
     * @param i a position known NOT to hold a stale entry. The
     * scan starts at the element after i.
     *
     * @param n scan control: {@code log2(n)} cells are scanned,
     * unless a stale entry is found, in which case
     * {@code log2(table.length)-1} additional cells are scanned.
     * When called from insertions, this parameter is the number
     * of elements, but when from replaceStaleEntry, it is the
     * table length. (Note: all this could be changed to be either
     * more or less aggressive by weighting n instead of just
     * using straight log n. But this version is simple, fast, and
     * seems to work well.)
     *
     * @return true if any stale entries have been removed.
     */
    //掃描一部分表清理其中的就條目,
    //掃描log(n)個
    private boolean cleanSomeSlots(int i, int n) {
        //在這次清理過程中是否清理了部分槽
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            //從i的下一個開始查詢(第i個在呼叫這個方法前已經查詢)
            i = nextIndex(i, len);
            Entry e = tab[i];
            //key=e.get(),e不為null,但key為null
            if (e != null && e.get() == null) {
                n = len;
                //標記清理過某個槽
                removed = true;
                //清理過程
                i = expungeStaleEntry(i);
            }
            //只掃描log(n)個槽,一方面可以保證避免過多的空槽的堆積
            //一方面可以保證插入或者刪除的效率
            //因為刪除的時候會掃描兩個空槽之前的槽位
            //每個槽位全部掃描的話時間複雜度會高,
            //因為在expungeStaleEntry掃描當前位置到第一個空槽之間所有的舊槽
            //所以在這裡進行每個槽位的掃描會做很多重複的事情,效率低下
            //雖然掃描從i開始的log(n)也會有很多重複掃描,但是通過優良的雜湊演算法
            //可以減少雜湊衝突也就可以減少重複掃描的數量
        } while ( (n >>>= 1) != 0);
        return removed;
    }

    /**
     * Re-pack and/or re-size the table. First scan the entire
     * table removing stale entries. If this doesn't sufficiently
     * shrink the size of the table, double the table size.
     */
    private void rehash() {
        expungeStaleEntries();

        // Use lower threshold for doubling to avoid hysteresis
        if (size >= threshold - threshold / 4)
            resize();
    }

    //擴容至當前表的兩倍容量
    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;

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

    /**
     * Expunge all stale entries in the table.
     */
    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);
        }
    }
}

一些問題


  • 為什麼使用弱引用:
    因為使用弱引用不會影響ThreadLocal物件被釋放後的垃圾回收,由於使用了弱引用,
    被釋放的物件只能存活到下一次gc,物件被回收後弱引用就變為null,這時候就可以進行判斷這個位置的條目是否已經是舊條目,
    從而進行清理。防止記憶體洩漏,
    儘管軟引用的物件也可以被垃圾回收掉,但是物件會存活到記憶體不足這樣會造成記憶體洩漏
  • 如何解決舊的槽中Entry物件不被回收造成記憶體洩漏問題:在get()中碰到舊槽會通過expungeStaleEntry()方法來清理舊槽,
    清理完成後會繼續向後清理直到遇到第一個空槽,在此期間會進行雜湊重定位操作,
    將每個槽中的都西昂放在合適的位置以維持雜湊表的順序;
    set()方法中呼叫replaceStaleEntry-->expungeStaleEntry()清理整個執行區間內的舊槽,
    並呼叫cleanSomeSlots()迴圈掃描log(n)個槽位進行清理,使用優秀的雜湊演算法減少每次呼叫到expungeStaleEntry()重複清理同一區間的工作;

\(\color{#FF3030}{轉載請標明出處}\)

相關文章