ThreadLocal原始碼

頂風少年發表於2021-11-23

Thread和ThreadLocal的關係

初始化ThreadLocalMap和弱引用Entry
set方法與雜湊衝突
清理槽
get方法也會清理槽
擴容
手動清理的重要性

Thread和ThreadLocal的關係

每個Thread中都持有一個ThreadLocalMap的例項,ThreadLocalMap是ThreadLocal的內部類。當Thread中沒有ThreadLocalMap則需要先例項化ThreadLocalMap.

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;//該物件是ThreadLocal中的內部類ThreadLocalMap
}

public class ThreadLocal<T> {
    //計算出來的hash值用它來確定Entry存放到哪個雜湊槽
    private final int threadLocalHashCode = nextHashCode();
    //這是個固定值
    private static final int HASH_INCREMENT = 0x61c88647;
    //這個預設值是0,但new ThreadLocal後斷點看到的值不是0,這是因為這是一個靜態成員,在我們自己建立ThreadLocal前,main方法會先載入ThreadLocal給這個賦值了。
    private static AtomicInteger nextHashCode = new AtomicInteger();
    //每次呼叫該方法都會在原有的nextHashCode值上加上0x61c88647
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    //設定值
    public void set(T value) {
        Thread t = Thread.currentThread();//獲取當前執行緒。
        ThreadLocalMap map = getMap(t);//獲取當前執行緒的成員變數ThreadLocal.ThreadLocalMap threadLocals 
        if (map != null)
            map.set(this, value);//如果當前執行緒中的ThreadLocalMap已經例項化則set
        else
            createMap(t, value);//如果當前執行緒中的ThreadLocalMap沒有例項化則例項化。
    }
    
    //在這走例項化ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

初始化ThreadLocalMap和弱引用Entry

ThreadLocalMap裡最重要的屬性是Entry[],這個陣列的初始長度是16,擴容閾值是size*2/3,Entry是ThreadLocalMap的內部類,Entry繼承了弱引用。Entry裡的key是ThreadLocal,value是設定的值。如果ThreadLocal棧引用結束了,在發生GC時雖然Entry還持有ThreadLocal的引用,這個ThreadLocal也會被垃圾回收,所以ThreadLocalMap常常伴隨著擴容,清理操作。

static class ThreadLocalMap {
    //繼承WeakReference很重要,WeakReferences是弱引用,在每次GC後都會回收弱引用物件裡的引用值(若通過可達性分析查到引用值沒有其他可達的Root,則會回收)
    //這個Entry就構成了唯一的key,也就是ThreadLocal。value是ThreadLocal.set(parameter)的引數
    static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);//最終傳遞給了Reference中的referent
                value = v;
            }
    }
    
    //ThreadLocalMap中的容器,一個執行緒持有一個ThreadLocalMap就相當於持有了一個Entry陣列
    private Entry[] table;
    
    //陣列的初始容量
    private static final int INITIAL_CAPACITY = 16;
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];//例項化陣列
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//確定陣列的位置
            //初始化ThreadLocalMap不會出現hash衝突。
            table[i] = new Entry(firstKey, firstValue);
            //已有元素++
            size = 1;
            //計算擴容閾值
            setThreshold(INITIAL_CAPACITY);
    }
    
    private void setThreshold(int len) {
            //初始化的容量第一次擴容的閾值是10,也就是說在陣列的size是10的情況下就會觸發擴容。
            threshold = len * 2 / 3;
    }
  }
}

set方法與雜湊衝突

ThreadLocal的set方法是使用ThreadLocalMap的set方法。他分為四種情況。1 計算雜湊後確定的槽內是null沒有Entry表示沒有雜湊衝突,此時new一個Entry放入槽內。 2 計算雜湊後確定的槽內有Entry但是槽內的Entry的key和當前的ThreadLocal相同則直接替換value。

3 計算雜湊後確定的槽內有Entry但是key和當前ThreadLocal並不是同一個,則表示雜湊衝突,此時順著陣列往右尋找,直到碰到有Entry但是沒有key的槽,這表示這個槽內曾經有過ThreadLocal但是被GC掉了,此時這個槽是個廢槽,可以替換掉Entry。 4 雜湊衝突後向右

並沒有找到被GC的槽,此時只能是找到距離最近的一個槽內沒有Entry的,建立一個Entry存入。

static class ThreadLocalMap {
  //順著當前下標往後查詢。如果查詢到了陣列末尾則返回0號下標
    private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
    }
    
    //順著當前下標往前查詢。如果已經是0則返回陣列末尾下標
    private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
    
    private void set(ThreadLocal<?> key, Object value) {
            //拿到陣列
            Entry[] tab = table;
            //陣列長度
            int len = tab.length;
            //hash&length-1 效果類似hash%length
            int i = key.threadLocalHashCode & (len-1);
            //在這就要處理hash衝突了。如果hash值不衝突,那麼算出來的index位置的Entry肯定是null.那麼不會進入迴圈。
            //如果進入了迴圈,有沒有可能兩個if都不滿足,有可能。這表示hash值衝突了,但是不是同一個ThreadLocal,並且hash值相同的槽內的ThreadLocal沒有被GC。
            //那麼只能是一直找到Entry是null的位置,然後跳出迴圈。
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //如果是第一次迴圈到這裡進去了,表示是同一個ThreadLocal多次設定值。則直接替換值。情況2
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果ThreadLocal為null則表示發生了GC把弱引用ThreadLocal清理了。
                //需要將當前set的key和value放入這個廢掉的槽內,並且看看有沒有需要清理的槽。情況3
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //沒有進入迴圈,或者從迴圈跳出了。如果沒有進入迴圈則i就是hash&length-1的位置表示當前算出來的hash值沒有衝突,也是第一次使用。情況1
            //如果是迴圈跳出來的,則這個i就是hash&length-1.算出來的位置向後移動迴圈次數的位置。表示hash衝突了,並且衝突後的槽往後也都沒有被GC
            //只能是往後順延找別的可用槽。總之會找到一個在陣列內Entry為空的位置。建立Entry放進陣列。情況4
            tab[i] = new Entry(key, value);
            //已有元素++
            int sz = ++size;
       //如果沒有清理槽,並且當前長度已經大於等於了閾值則擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } //走到這個方法表示通過hash&length-1的位置上的Entry中的key是null或者是雜湊衝突後,往陣列後查詢發現有Entry中的key是null private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { //陣列 Entry[] tab = table; //陣列長度 int len = tab.length; Entry e; //Entry為null的雜湊槽 int slotToExpunge = staleSlot; //從Entry為null的雜湊槽位置向前找,一直找到Entry為null停止 for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)){ //在向前尋找的過程中標記Entry中key為null的下標 if (e.get() == null) slotToExpunge = i; } //從Entry為null的雜湊槽位置向後找,一直找到Entry為null停止 for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) { //迴圈中的Entry中的key ThreadLocal<?> k = e.get(); if (k == key) { //如果key相同則替換value e.value = value; //將Entry中ThreadLocal為null的賦值給當前槽中 tab[i] = tab[staleSlot]; //在將Entry賦值給原來ThreadLocal為null的槽中。 //這兩行操作相當於把槽裡的內容互換了,達到的效果是前邊的槽中的Entry有key,迴圈中的也就是後邊的沒有key tab[staleSlot] = e; //如果列表向左查詢沒有發現Entry中key有null的。則將當前迴圈中的槽的位置賦值。 //因為上兩步操作已經把當前槽變成了key為null的槽,所以此處記錄的位置就是key是null的位置 //如果向左查詢有Entry裡是null值那就表示這個區間內還有更左邊有key是null的 if (slotToExpunge == staleSlot){ slotToExpunge = i; } //清理槽 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //當前迴圈中的槽也是被GC過的。並且向左查詢沒有發現Entry為null的,就記錄當前槽的位置。 if (k == null && slotToExpunge == staleSlot){ slotToExpunge = i; } } //出迴圈只有一種情況,key為null的Entry下標往後尋找沒有發現與當前ThreadLocal相同的key。 //此時需要將原來Entry的value職位null。此操作用來釋放記憶體。 tab[staleSlot].value = null; //建立一個新的Entry其中key是當前ThreadLocal,value是set的引數。將它放到被GC的位置。 tab[staleSlot] = new Entry(key, value); //如果向左查詢有Entry中key是null的slotToExpunge就是在左邊確定的 //如果向左查詢沒有Entry中key是null的,而向右查詢有Entry中key是null的slotToExpunge就是右邊確定的。 //如果兩邊都沒有的情況表示當前區間內只有staleSlot一個為Entry是null的而這種情況下直接重新覆蓋了Entry。不需要清理。條件不成立。 if (slotToExpunge != staleSlot){ cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } } }

清理槽

expungeStaleEntry方法就是將廢槽清空,然後將雜湊衝突的槽重新分配位置,因為雜湊衝突後是從雜湊位向後移動尋找Entry是null的槽放入的,此後這些衝突的槽可能有被清理的,所以重新分配位置,方法的返回值是Entry為null的位置,cleanSomeSlots方法從這個位置

繼續尋找有沒有廢槽,如果有就清理。

static class ThreadLocalMap {
      //接收的引數是槽裡沒有Entry的槽和當前陣列的長度
    private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //找到下一個槽的位置
                i = nextIndex(i, len);
                //獲取槽內的Entry
                Entry e = tab[i];
                //如果槽內有Entry,並且Entry的key是null,表示這是個廢槽。
                if (e != null && e.get() == null) {
                    n = len;
                    //有廢槽肯定要清理的。
                    removed = true;
                    //方法返回下一個槽內沒有Entry的槽下標
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);//這個操作相當於折半除2的操作。10,5,2,0,
            return removed;
    }
    
    //接收的引數是槽下標內有Entry,但是Entry的key被GC了。
    private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            //將槽清空
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            //Entry[]--
            size--;
            Entry e;
            int i;
            //迴圈的開始是廢槽的下一個,終止條件是下一個槽有Entry
            for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
                //拿到槽內的ThreadLocal
                ThreadLocal<?> k = e.get();
                //如果槽內的key也是null則表示這也是個廢槽,則也需要做清空操作。
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //如果槽內有的Entry有key,則通過hash值算出槽的位置。
                    int h = k.threadLocalHashCode & (len - 1);
                    //如果算出的槽位置不是當前的位置則表示這個key曾經雜湊衝突了,所以位置並不是雜湊位。
                    if (h != i) {
                        //將這個槽清空
                        tab[i] = null;
                        //從計算的雜湊位開始迴圈,找到Entry為null的槽,將剛剛清空槽裡的Entry重新安置。
                        while (tab[h] != null){
                            h = nextIndex(h, len);
                        }
                        //這一步的操作的意義在於,如果迴圈中有if條件滿足的,這代表當前i這個位置之前有可用的槽,那就從雜湊位開始往後找,找到空槽,重新安置這個Entry。
                        tab[h] = e;
                    }
                }
            }
            return i;//入參staleSlot是一個廢槽,返回的i則是一個Entry為null的槽。
    }      
}

get方法也會清理槽

get方法通過當前ThreadLocal獲取Entry[]中對應的Entry,如果ThreadLocalMap未例項化則例項化並返回null,通過雜湊位找到了就返回,雜湊位上的不是當前ThreadLocal則表示雜湊衝突,繼續在陣列後尋找,如果途中發現有廢槽則清理,如果最終沒有找到則返回null。

public class ThreadLocal<T> {
       public T get() {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //獲取執行緒內的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果ThreadLocalMap已經例項化
        if (map != null) {
            //通過ThreadLocal這個key到陣列中找到Entry,是有可能找不到返回null的
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果找到了,返回Entry中的value
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //走到這裡兩種情況,1 ThreadLocalMap沒有例項化,則例項化 2 從Entry[]沒有找到對應ThreadLocal的Entry
        return setInitialValue();
    }
    
    //這個方法和set差不多,但是它可以返回null。
    private T setInitialValue() {
        //如果現在使用的就是ThreadLocal則一定返回null.
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    //這個方法只能子類重寫,意味著可以給ThreadLocal賦預設值。
    protected T initialValue() {
        return null;
    }  
}

static class ThreadLocalMap {
    static class ThreadLocalMap {

    //通過ThreadLocal找Entry
     private Entry getEntry(ThreadLocal<?> key) {
            //計算雜湊位
            int i = key.threadLocalHashCode & (table.length - 1);
            //檢視雜湊位上的Entry
            Entry e = table[i];
            //如果Entry不是null或者Entry的key就是當前的ThreadLocal則找到了返回Entry
            if (e != null && e.get() == key){
                return e;
            }
            else{
                //如果從雜湊位沒有找到Entry或者Entry中的key不是當前ThreadLocal
                return getEntryAfterMiss(key, i, e);
            }
    }
    //接收的引數是當前ThreadLocal,計算的雜湊位,和這個雜湊位上的Entry
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            //從雜湊位上開始迴圈尋找
            while (e != null) {
                ThreadLocal<?> k = e.get();
                //如果找到了key相同的則返回
                if (k == key){
                    return e;
                }    
                //如果當前槽內的key是null則要被清理
                if (k == null){
                    expungeStaleEntry(i);
                }else{
                    //如果槽內的key有值則繼續尋找。直到Entry位null停止。
                    i = nextIndex(i, len);
                }
                //下一個位置繼續找
                e = tab[i];
            }
            //如果迴圈結束了,表示雜湊位往後尋找的key都不是當前的ThreadLocal,返回null。
            return null;
    }    
}

擴容

當陣列內的元素到達閾值後觸發擴容,擴容操作進行前會遍歷陣列進行清理。如果清理後仍然達到閾值則二倍擴容,迴圈擴容前的陣列,根據新陣列的長度重新計算雜湊值,如果雜湊槽內沒有元素則放入,如果有則線性查詢可用槽放入。然後用新的陣列替換老的陣列。

 

static class ThreadLocalMap {
   //擴容
         private void rehash() {
            //清理一遍槽
            expungeStaleEntries();
            //大於閾值擴容
            if (size >= threshold - threshold / 4)
                resize();
        }
        
        //全部清理
        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);
                }
            }
        }
        
        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];
                //如果Entry不是null
                if (e != null) {
                    //獲取key
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        //key是null清理
                        e.value = null; 
                    } else {
                        //根據雜湊值算出來在新的陣列中的位置。
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //新的位置上有Entry表示雜湊衝突,則繼續向後尋找。
                        while (newTab[h] != null){
                            h = nextIndex(h, newLen);
                        }
                        //找到一個Entry為null的位置存放Entry。
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            //設定新的閾值
            setThreshold(newLen);
            //新陣列內元素的總個數
            size = count;
            //替換陣列
            table = newTab;
        }    
}

手動清理的重要性

clear方法就是把ThreadLocal從Entry中刪除,然後刪除Entry。這樣Entry就沒有了引用會被GC。如果不使用clear,那麼就算是ThreadLocal棧記憶體釋放了,這個物件還是存在於Thread裡的ThreadLocalMap裡的Entry[]陣列中,除非遇到GC否則永遠存在。手動清理的作用就在於不用等待GC自己把Entry清理。

public class ThreadLocal<T> {
  //通過ThreadLocalMap的remove方法釋放記憶體
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }  
}

static class ThreadLocalMap {
   //通過當前ThreadLocal刪除
    private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //計算雜湊位
            int i = key.threadLocalHashCode & (len-1);
            //迴圈找匹配的key
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //呼叫Refereence的clear把key清空
                    e.clear();
                    //再次清理槽。
                    expungeStaleEntry(i);
                    return;
                }
            }
    }
}

public abstract class Reference<T> {
  private T referent;//這個就是ThreadLocal物件
    public void clear() {
        this.referent = null;
    }  
}

 

 

 

相關文章