ThreadLocal 從源碼角度簡單分析

ThreadLcoal源碼淺析

咱們知道ThreadLocal用於維護多個線程線程獨立的變量副本,這些變量只在線程內共享,可跨方法、類等,以下是一個維護多個線程Integer變量的ThreadLocal:數組

ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();

每一個使用threadLocalNum的線程,能夠經過形如threadLocalNum.set(1)的方式建立了一個獨立使用的Integer變量副本,那麼它是怎麼實現的呢?咱們今天就來簡單的分析一下。安全

先看下ThreadLocal的set方法是如何實現的,源碼以下:this

public void set(T value) {
        Thread t = Thread.currentThread();  //獲取當前線程
        ThreadLocalMap map = getMap(t);     //獲取當前線程的ThreadLocalMap
        if (map != null)
            map.set(this, value);           //當前線程的ThreadLocalMap不爲空則直接設值
        else
            createMap(t, value);            //當前線程的ThreadLocalMap爲空則建立一個來設置值
    }

是的,你沒有看錯,是獲取當前線程中的ThreadLocalMap來設置的值,咱們來看一下getMap(t)是如何實現的:線程

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

而後咱們看到Thread中包含了一個ThreadLocalMap類型的屬性:設計

ThreadLocal.ThreadLocalMap threadLocals = null;

到這裏咱們能夠得出一個結論:各個線程持有了一個ThreadLocalMap的屬性,經過ThreadLocal設置變量時,直接設置到了對應線程的的ThreadLocalMap屬性中code

那麼不一樣的線程中經過ThreadLocal設置的值是如何關聯定義的ThreadLocal變量和Thread中的ThreadLocalMap的呢?咱們接着分析。對象

前面寫到當前線程的ThreadLocalMap爲空則建立一個ThreadLocalMap來設值,咱們來看下createMap(t, value)的具體實現:blog

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

///////////////////
//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);
        }
private static final int INITIAL_CAPACITY = 16;

線程中threadLocals是一個ThreadLocalMap變量,其默認值是null,該線程在首次使用threadLocal對象調用set的時候經過createMap(Thread t, T firstValue)實例化。繼承

先來看一下ThreadLocalMap,它是在ThreadLocal中定義的一個靜態內部類,其內屬性以下:

/**
         * 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;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

其中屬性private Entry[] table,用於存儲經過threadLocal set 進來的變量,Entry定義以下:

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

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

Entry繼承了WeakReference<ThreadLocal<?>>,ThreadLocal在構造器中被指定爲弱引用super(k)(後面會單獨討論爲什麼這裏使用弱引用)。

至此,咱們能夠知道ThreadLocal和Thead的內存結構以下:

ThreadLocal的垃圾回收

網上看到不少文章都在講ThreadLocal的內存泄露問題,因此也在這裏簡單說一下本身的理解。

從上面的結構能夠看出ThreadLocal涉及到的要回收的對象包括:

  • ThreadLocal實例自己
  • 各線程中的threadLocalMap,其中包括各個Entry的 key, value

下面先簡述java的引用,而後分別討論ThreadLocal自己的回收和threadLcoalMap的回收

Java引用

  • 強引用(StrongReference):對象可達就不會被gc回收,空間不足時報error
  • 軟引用(SoftReference):對象無其餘強引用,當空間不足時纔會被gc回收。
  • 弱引用(WeakReference):對象無其餘強引用,gc過程掃描到就會被回收。

ThreadLocal的回收

ThreadLocal實例的引用主要包括兩種:

  • ThreadLocal定義處的強引用
  • 各線程中ThreadLocalMap裏的key=weak(threadLocal), 是弱引用

強引用還在的狀況下ThreadLocal必定不會被回收;無強引用後,因爲各個Thread中Entry的key是弱引用,會在下次GC後變爲null。ThreadLocal實例何時被回收徹底取決於強引用什麼時候被幹掉,那麼何時強引用會被銷燬呢?最簡單的就是 threadLocal=null強引用被賦值爲null;其它也但是threadLocal是一個局部變量,在方法退出後引用被銷燬,等等。

這裏來回答一下前面提到的爲何ThreadLocalMap中將key設計爲弱引用,咱們假設若是ThreadLocalMap中是強引用會出現什麼狀況?定義ThreadLocal時定義的強引用被置爲null的時候,若是還有其它使用了該ThreadLocal的線程沒有完成,還須要好久會執行完成,那麼這個線程將一直持有該ThreadLocal實例的引用,直到線程完成,期間ThreadLocal實例都不能被回收,最重要的是若是不瞭解ThreadLocal內部實現,你可能都不知道還有其餘線程引用了threadLocal實例。

線程結束時清除ThreadLocalMap的代碼Thread.exit()以下:

/**
     * This method is called by the system to give a Thread
     * a chance to clean up before it actually exits.
     */
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

各線程中threadLocalMap的回收

單從引用的角度來看,各線程中的threadLocalMap,其中包括各個Entry的key 和 value,線程(也就是Thread實例)自己一直持有threadLocalMap的強引用,只有在線程結束的時候纔會被回收,可是ThreadLocal在實現的時候提供了一些方法:set/get/remove,能夠在執行它們的時候回收其它已經失效(key=null)的entry實例。

這裏就以set爲例看看ThreadLocal是如何回收entry的,ThreadLocal set方法實現以下:

//ThreadLocal
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);  // 本次要分析的方法
        else
            createMap(t, value);   //這裏前面已經分析了
    }

//ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);  //獲取當前threadLocal實例的hashcode,同時也是table的下標

            //這裏for循環找key,是由於hash衝突會使hashcode指向的下標不是真實的存儲位置
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) { 
                ThreadLocal<?> k = e.get();
                //找到了設置爲新值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //entry不爲null,key爲null
                //說明原來被賦值過,可是原threadLocal已經被回收
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //若是下標對應的entry爲null, 則新建一個entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清理threadlocal中其它被回收了的entry(也就是key=null的entry)
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                //rehash
                rehash();
        }

看一下cleanSomeSlots的實現:

//ThreadLocalMap
private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //獲取下一個entry的下標
                i = nextIndex(i, len);
                Entry e = tab[i];
                //entry不爲null,key爲null
                //說明原來被賦值過,可是原threadLocal已經被回收
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    // 刪除已經無效的entry
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }



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

            // 回收無效entry
            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();
                //entry不爲null,key爲null,應該回收
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //rehash的實現
                    //計算當前entry的k的hashcode,看是下標是否應該爲i
                    //若是不爲i說明,是以前hash衝突放到這兒的,如今須要reash
                    int h = k.threadLocalHashCode & (len - 1);
                    //h!=i 說明hash衝突了, entry不該該放在下標爲i的位置
                    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;
        }

從上面的分析咱們能夠看到把ThreadLocalMap中的key設計爲weakReference,也使set方法能夠經過key==null && entry != null判斷entry是否失效

總結一下ThreadLocal set方法的實現:

  • 根據threadLocal計算hashcode找到entry[]數組對應位置設置值
  • 遍歷數組找到其它失效的(entry不爲null,key爲null)的entry刪除

內存泄露問題

ThreadLocal經過巧妙的設計最大程度上減小了內存泄露的可能,可是並無徹底消除。

當咱們使用完ThreadLocal後沒有調用set/get/remove方法,那麼可能會致使失效內存不能及時被回收,致使內存泄露,尤爲是在value佔用內存較大的狀況。

因此最佳實踐是,在明確ThreadLocal再也不使用時,手動調用remove方法及時清空。

總結

  • ThreadLocal 並不解決線程間共享數據的問題
  • ThreadLocal是經過讓線程內的ThreadLocalMap.Entry的key指向自身,來實現了對線程內對象的引用,從而能夠在線程內方便的使用變量。同時由於操做的都是線程內的變量,也避免了實例線程安全的問題
  • ThreadLocal 適用於變量在線程間隔離且在方法間共享的場景
  • ThreadLocalMap 的 Entry 對 ThreadLocal 的引用爲弱引用,避免了 ThreadLocal 對象沒法被回收的問題
  • ThreadLocalMap 的 set 方法經過調用 cleanSomeSlots 方法回收鍵爲 null 的 Entry 對象的值(即失效實例)從而防止內存泄漏(其它的remove,get相似)
  • 在明確ThreadLocal再也不使用時,手動調用remove方法及時清空

參考

正確理解Thread Local的原理與適用場景

相關文章
相關標籤/搜索