以後再也不用擔心提起ThreadLocal兩眼乾瞪的窘境了

1.背景

什麼是ThreadLocal?

    ThreadLocal類可以理解爲線程本地變量。也就是說如果定義了一個ThreadLocal,每個線程往這個ThreadLocal中讀寫是線程隔離,互不影響的。它提供了一種將可變數據通過每個線程有自己的獨立副本從而實現線程封閉的機制。

它的實現思路是什麼?

   ThreadLocal類有一個類型爲ThreadLocal.ThreadLocalMap,也就是說每個線程都有一個自己的ThreadLocalMap。ThreadLocalMap有自己的獨立實現,可以將ThreadLocal視作它的key,value爲代碼中放入的值(其實實際上key並不是ThreadLocal本身,而是它的一個弱引用《這涉及到JVM的回收機制,下一篇博客會闡述》)。每個線程往Thread Local中存值都是存在了ThreadLocalMap中。當取值時也是以ThreadLocal爲key(一個弱引用),然後找到對應的值,從而實現了線程隔離。

 

2.Thread和ThreadLocal有什麼聯繫?

Thread和ThreadLocal是綁定的,ThreadLocal依賴於Thread去執行,Thread將需要隔離的數據存放到ThreadLocal(ThreadLocalMap)中,來實現多線程處理。

 

3.ThreadLocal的內存泄露

    關於ThreadLocal內存泄漏問題,網上一搜一大堆,也是面試經常會問到的問題。其實看過ThreadLocal的源碼的開發大都知道爲什麼一個類似Map的存儲結構怎麼會出現內存泄漏的問題。

 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;
            }
        }
    ...

   其實主要是ThreadLocalMap中Entry的設計,Entry繼承了WeakReference<ThreadLocal<?>>,而它的key值賦值則調用了父類的有參構造,即Entry的key是一個弱引用,所以key會在垃圾回收的時候被回收,而key對應的value則不會被回收,即Entry的key 爲null,但value是有值的,如此往復,value累加就會導致內存泄漏。

這個問題我想當時設計ThreadLocal類的Josh Bloch、Doug Lea這兩大神估計也想到了:

//刪除無效的value  
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 {
                    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.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

這個expungeStaleEntry方法就是用來清除key爲null而value有值的無效數據的,閱讀源碼後發現,ThreadLocal的API中的get()、set()、remove()方法中都間接調用了expungeStaleEntry(int staleSlot)這個方法。下面以remove()方法爲例,看看源碼:

 
 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
ThreadLocal類中的remove()方法又調用了ThreadLocalMap中的remove()方法


       /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

所以閱讀過源碼之後我們可以推斷:在使用ThreadLocal的過程中,用完之後調用remove方法是個很好的編程習慣,這樣就可以避免內存泄漏。

那麼問題來了,既然弱引用導致內存泄漏,那爲什麼不把key設置爲強引用?

如果key設置爲強引用,當ThreadLocal實例釋放後,threadlocal就成Null了,但是ThreadLocal還有強引用指向ThreadLocalMap,而ThreadLocalMap.Entry又強引用ThreadLocal,這樣就會導致GC不能正常回收ThreadLocal,又會導致內存泄漏

4.擴展---spring如何保證數據庫事務在同一個連接下執行?

DataSourceTransactionManager是spring的數據源事務管理器,它會在你調用getConnection()的時候從數據庫連接池中獲取一個connection,然後將其與ThreadLocal綁定,事務完成後解除綁定。這樣就保證了事務在同一個連接下完成。

 

 

越努力,越幸運!

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