ThreadLocal原理分析

本文結構

  • 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對應的值能夠不同。less

ThreadLocal實現原理


ThreadLocal中的字段與構造方法

詳細說明參考註釋ide

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)方法this

//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()方法spa

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}{轉載請標明出處}\)

相關文章
相關標籤/搜索