1. 程式人生 > >多線程學習筆記九之ThreadLocal

多線程學習筆記九之ThreadLocal

當我 use gen nal get 內存泄漏 ble tables 目錄

目錄

  • 多線程學習筆記九之ThreadLocal
    • 簡介
      • 類結構
    • 源碼分析
      • ThreadLocalMap
      • set(T value)
      • get()
      • remove()
    • 為什麽ThreadLocalMap的鍵是WeakReferrence?
    • 總結

多線程學習筆記九之ThreadLocal

簡介

??ThreadLocal顧名思義理解為線程本地變量,這個變量只在這個線程內,對於其他的線程是隔離的,JDK中對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提供了線程局部變量,只能通過ThreadLocal的set方法和get方法來存儲和獲得變量。

類結構

??ThreadLocal類結構如下:

技術分享圖片

可以看到ThreadLocal有內部類ThradLocalMap,ThreadLocal存儲線程局部對象就是利用了ThreadLocalMap數據結構,在下面的源碼分析也會先從這裏開始。

源碼分析

ThreadLocalMap

??ThreadLocalMap靜態內部類Entry是存儲鍵值對的基礎,Entry類繼承自WeakReference(為什麽用弱引用在後面解釋),通過Entry的構造方法表明鍵值對的鍵只能是ThreadLocal對象,值是Object類型,也就是我們存儲的線程局部對象,通過super調用父類WeakReference構造函數將ThreadLocal<?>對象轉換成弱引用對象

??ThreadMap存儲鍵值對的原理與HashMap是類似的,HashMap依靠的是數組+紅黑樹數據結構和哈希值映射,ThreadMap依靠Entry數組+散列映射,ThreadLocalMap使用了Entry數組來保存鍵值對,Entry數組的初始長度為16,鍵值對到Entry數組的映射依靠的是int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);,通過ThreadLocal對象的threadLocalHashCode與(INITIAL_CAPACITY - 1)按位相與將鍵值對均勻散列到Entry數組上。

    static class ThreadLocalMap {

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

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

        //初始Entry數組大小
        private static final int INITIAL_CAPACITY = 16;

        //Entry數組
        private Entry[] table;

        //ThreadLocalMap實際存儲鍵值對的個數
        private int size = 0;

        //數組擴容閾值
        private int threshold; // Default to 0

        //閾值為數組長度的2/3
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        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);
        }
        
        //構造一個ThreadLocalMap對象,並把傳入的第一個鍵值對存儲
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

    }

??ThreadLocal作為做為鍵值對的鍵通過常量threadLocalHashCode映射到Entry數組,threadLocalHashCode初始化時會調用nextHashCode()方法,就是在nextHashCode的基礎上加上0x61c88647,實際上每個ThreadLocal對象的threadLocalHashCode值相差0x61c88647,這樣生成出來的Hash值可以較為均勻的散列到2的冪次方長度的數組中,具體可見這篇文章為什麽使用0x61c88647
??由於采用的是散列算法,就需要考慮Hash沖突的情況,HashMap解決Hash沖突的方法是鏈表+紅黑樹,ThreadLocalMap解決方法是linear-probe(線性探測),簡單來說如果散列對應的位置已經有鍵值對占據了,就把散列位置加/減一找到符合條件的位置放置鍵值對。

    // final常量,一旦確定不再改變
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    //構造方法
    public ThreadLocal() {
    }

set(T value)

??簡要介紹完了內部類ThreadLocalMap後,set方法屬於ThreadLocal,首先獲得與線程Thread綁定的ThreadLocalMap對象,再將ThreadLocal和傳入的value封裝為Entry鍵值對存入ThreadLocalMap中。註意,ThreadLocalMap對象是在線程Thread中聲明的:
ThreadLocal.ThreadLocalMap threadLocals = null;

    public void set(T value) {
        //獲得當前線程對象
        Thread t = Thread.currentThread();
        //獲得線程對象的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 如果map存在,則將鍵值對存到map裏面去
        if (map != null)
            map.set(this, value);
        //如果不存在,調用ThreadLocalMap構造方法存儲鍵值對
        else
            createMap(t, value);
    }

    //返回線程t中聲明的Thread
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • set(ThreadLocal<?> key, Object value)
    ??在ThreadLocalMap存在的情況下,調用ThreadLocal類的set方法存儲鍵值對,set方法需要考慮散列的位置已經有鍵值對:如果已經存在的鍵值對的鍵當存入的鍵,覆蓋鍵值對的值;如果鍵值對的鍵ThreadLocal對象已經被回收,調用replaceStaleEntry方法刪除table中所有陳舊的元素(即entry的引用為null)並插入新元素。
    private void set(ThreadLocal<?> key, Object value) {        
        Entry[] tab = table;
        int len = tab.length;
        //利用ThreadLocal的threadLocalHahsCode值散列
        int i = key.threadLocalHashCode & (len-1);

        //如果散列的位置不為空,判斷是否是哈希沖突
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            
            //如果此位置的鍵值對的鍵與傳入的鍵相同,覆蓋鍵值對的值
            if (k == key) {
                e.value = value;
                return;
            }

            //鍵值對的鍵為空,說明鍵ThreadLocal對象被回收,用新的鍵值對代替過時的鍵值對
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        //散列位置為空,直接存儲鍵值對
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

get()

??獲得當前線程中保存的以ThreadLocal對象為鍵的鍵值對的值。首先獲取當前線程關聯的ThreadLocalMap,再獲得以當前ThreadLocal對象為鍵的鍵值對,map為空的話返回初始值null,即線程局部變量為null,

    public T get() {
        //獲取與當前線程綁定的ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //map不為空,獲取鍵值對對象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    private Entry getEntry(ThreadLocal<?> key) {
        //散列
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        //判斷散列位置的鍵值對是否符合條件:e.get()==key
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

    //線性探測尋找key對應的鍵值對
    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)
                return e;
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

remove()

??從ThreadLocalMap中移除鍵值對,一般在get方法取出保存的線程局部變量後調用remove方法防止內存泄露。

    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }

為什麽ThreadLocalMap的鍵是WeakReferrence?

??鍵值對對象Enry的鍵是ThreadLocal對象,但是使用WeakReferrence虛引用包裝了的,虛引用相對於我們經常使用的String str = "abc"這種強引用來說對GC回收對象的影響較小,以下是虛引用的介紹:

WeakReference是Java語言規範中為了區別直接的對象引用(程序中通過構造函數聲明出來的對象引用)而定義的另外一種引用關系。WeakReference標誌性的特點是:reference實例不會影響到被應用對象的GC回收行為(即只要對象被除WeakReference對象之外所有的對象解除引用後,該對象便可以被GC回收),只不過在被對象回收之後,reference實例想獲得被應用的對象時程序會返回null。

??如果Entry的鍵使用強引用,那麽我們存入的鍵值對即使線程之後不再使用也不會被回收,生命周期將變得和線程的生命周期一樣。而使用了虛引用之後,作為鍵的虛引用並不影響ThreadLocal對象被GC回收,當ThreadLocal對象被回收後,鍵值對就會被標記為stale entry(過期的鍵值對),再下一次調用set/get/remove方法後會進行??ThreadLocalMap層面對過期鍵值對進行回收,防止發生內存泄漏。
註意:當我們使用了set方法存入局部變量後,如果不進行get/remove,那麽過期的鍵值對無法被回收,所以建議在get取出存儲變量後手動remove,可以有效防止內存泄漏。

總結

??ThreadLocal實現了存儲線程局部變量,ThreadLocal的實現並不是HashMap<Thread,Object>以線程對象為鍵,而是在線程內部關聯了一個ThreadLocalMap用於存儲鍵值對,鍵值對的鍵是ThreadLocal對象,所以ThreadLocal對象本身是不存儲內容的,而是作為鍵與存儲內容構成鍵值對。

多線程學習筆記九之ThreadLocal