深入解析ThreadLocal源碼

概念

ThreadLocal 是線程的局部變量, 是每一個線程所單獨持有的,其他線程不能對其進行訪問。

當使用ThreadLocal維護變量的時候 爲每一個使用該變量的線程提供一個獨立的變量副本,即每個線程內部都會有一個該變量,這樣同時多個線程訪問該變量並不會彼此相互影響,因此他們使用的都是自己從內存中拷貝過來的變量的副本, 這樣就不存在線程安全問題,也不會影響程序的執行性能。

threadlocal,ThreadLocalMap,thread 三者關係圖

在這裏插入圖片描述

ThreadLocal的數據結構

Thread類中有個變量threadLocals,這個類型爲ThreadLocal中的一個內部類ThreadLocalMap,這個類沒有實現map接口,就是一個普通的Java類,但是實現的類似map的功能。

ThreadLocal數據結構圖
在這裏插入圖片描述

每個線程都要自己的一個map,map是一個數組的數據結構存儲數據,每個元素是一個Entry,entry的key是threadlocal的引用,也就是當前變量的副本,value就是set的值。

ThreadLocal 源碼分析

1.set方法

//set 方法
public void set(T value) {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取到當前線程的 ThreadLocalMap 類型的變量 threadLocals
    ThreadLocalMap map = getMap(t);
    //如果存在則直接賦值
    if (map != null)
        map.set(this, value);
    else //如果不存在則給該線程創建 ThreadLocalMap 變量並賦值。
        createMap(t, value);
}

//獲取線程中的ThreadLocalMap 字段!!
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//創建線程的變量
void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
}

總結:

對象實例與 ThreadLocal 變量的映射關係是存放的一個 Map 裏面(這個 Map 是個抽象的 Map 並不是 java.util 中的 Map ),而這個 MapThread 類的一個字段!而真正存放映射關係的 Map 就是 ThreadLocalMap

2.get方法

public T get() {
    //獲取當前Thread
    Thread t = Thread.currentThread();
    //獲取到當前線程的 ThreadLocalMap 類型的變量 threadLocals
    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;
}

總結:

獲取當前線程的 ThreadLocalMap 變量,如果存在則返回值,不存在則創建並返回初始值。

ThreadLocalMap 源碼分析

ThreadLocal 的底層實現都是通過 ThreadLocalMap 來實現的

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;
    
    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;
}

table

存放對象實例與變量的關係,並且實例對象作爲 key,變量作爲 value 實現對應關係。這裏的 key 採用的是對實例對象的弱引用,(因爲我們這裏的 key 是對象實例,每個對象實例有自己的生命週期,這裏採用弱引用就可以在不影響對象實例生命週期的情況下對其引用)。

Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

WeakReference 兩個構造函數

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

1.WeakReference(T referent):referent就是被弱引用的對象(注意區分弱引用對象和被弱引用的對應,弱引用對象是指WeakReference的實例或者其子類的實例)

2.WeakReference(T referent, ReferenceQueue<? super T> q):與上面的構造方法比較,多了個ReferenceQueue,在對象被回收後,會把弱引用對象,也就是WeakReference對象或者其子類的對象,放入隊列ReferenceQueue中,注意不是被弱引用的對象,被弱引用的對象已經被回收了。

從上可知,這是個弱引用,意味這可能會被垃圾回收器回收掉,threadLocal.get()==null,也就意味着被回收掉了

1.set方法

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    //獲取 hash 值,用於數組中的下標
    int i = key.threadLocalHashCode & (len-1);
		
    //如果數組該位置有對象則進入
    //通過hash尋找下標,尋找相等的ThreadLocal對象
    //1.找到相同的對象
    //2.一直往數組下一個下標查詢,指導下一個下標對應的爲null,退出循環
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        //k 相等則覆蓋舊值
        if (k == key) {
            e.value = value;
            return;
        }

        //此時說明此處 Entry 的 k 中的對象實例已經被回收了,需要替換掉這個位置的 key 和 value
        if (k == null) {
            //key過期了,需要替換
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	//沒找到
    //創建 Entry 對象
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //擴容
        rehash();
}


//獲取 Entry
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);
}

//獲取從i到len的下一個增量
private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
 }

//擴容
private void rehash() {
            expungeStaleEntries();

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

//清除所有過時的對象 referent
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);
            }
        }

        /**
         * 2倍擴容
         */
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;
        }

        

replaceStaleEntry

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

    //從當前的staleSlot向前遍歷 i--;
    //爲了把前面所有的已經被垃圾回收的也一起是放空間出來
    //這裏只key被回收,value還沒被回收,entry更加沒回收,所依需要讓他們回收
    //同時也避免這樣存在很多過期的對象佔用,導致這個時候剛好來了一個新元素達到閾值而觸發一次心的rehash
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 這個時候是從數據下標小的往下標大的方向遍歷,i++剛好跟上面相反
    //這兩個遍歷爲了在左邊遇到第一個空的entry到右邊遇到第一個空的entry之間查詢所有過期的對象
    //在右邊如果找到需要設置值的key 相同的時候開始清理
    	然後返回,不再繼續遍歷
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

       //說明之前已經存在相同的key,所依需要替換舊的值並且和前面那個過期的對象交換位置
        if (k == key) {
            e.value = value;

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

            //前面的第一個for循環(i--)往前查找的時候沒有找到的過期的,只有taleSlot
            //這個過期由於前面過期的對象已經通過交換位置的方式到index=i上了,所以需要清理i,而不是staleslot
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            //清理過期數據
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        //如果在第一個for循環(i--)向前遍歷無任何過期對象
        //那麼我們需要把slotToExpunge設置爲後遍歷(i++)的第一個過期對象的位置
        //如果整個數組都沒找到要設置的key的時候,該key會設置在該staleslot的位置上
        //如果數組中存在要設置的key,那麼上面也會通過交換位置的時候把有效值設置到staleSlot位置上
        //綜上: staleSlot存放的是有效值,不需要被清理
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果key 在數組中不存在,則新建一個放進去
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果有其他已經過期的對象,則清理此過期對象
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}


//獲取前一個序號
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

expungeStaleEntry(int staleSlot)

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

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    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 {
        //採用開放地址法,刪除的元素是多個衝突元素重的一個,需要對後面的元素做處理(讓後面元素往前移動),這麼做,住要是開放地址法尋找元素的時候,遇到null就停止薰着了,你前面key=null的時候已經設置entery爲null,不移動後面的元素永遠訪問不了
            int h = k.threadLocalHashCode & (len - 1);
           //不相等說明hash是有衝突的
           if (h != i) {
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

ThreadLocal 應用場景

ThreadLocal 適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用(相同線程數據共享),也就是變量在線程間隔離(不同的線程數據隔離)而在方法或類間共享的場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章