ThreadLocal弱引用與內存泄漏分析

本文對ThreadLocal弱引用進行一些解析,以及ThreadLocal使用注意事項。java

ThreadLocal

首先,簡單回顧一下,ThreadLocal是一個線程本地變量,每一個線程維護本身的變量副本,多個線程互相不可見,所以多線程操做該變量沒必要加鎖,適合不一樣線程使用不一樣變量值的場景。數組

其實現原理這裏就不作詳細闡述,其數據結構是每一個線程Thread類都有個屬性ThreadLocalMap,用來維護該線程的多個ThreadLocal變量,該Map是自定義實現的Entry<K,V>[]數組結構,並不是繼承自原生Map類,Entry其中Key便是ThreadLocal變量自己,Value則是具體該線程中的變量副本值。結構如圖:數據結構

threadlocal.png

所以ThreadLocal其實只是個符號意義,自己不存儲變量,僅僅是用來索引各個線程中的變量副本。多線程

值得注意的是,Entry的Key即ThreadLocal對象是採用弱引用引入的,如源代碼:this

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

本文下面重點分析爲什麼使用弱引用,以及可能存在的問題。spa

首先看下弱引用。線程

弱引用

java語言中爲對象的引用分爲了四個級別,分別爲 強引用 、軟引用、弱引用、虛引用。代碼規範

其他三種具體可自行查閱相關資料。code

弱引用具體指的是java.lang.ref.WeakReference<T>類。對象

對對象進行弱引用不會影響垃圾回收器回收該對象,即若是一個對象只有弱引用存在了,則下次GC將會回收掉該對象(無論當前內存空間足夠與否)。

再來講說內存泄漏,假如一個短生命週期的對象被一個長生命週期對象長期持有引用,將會致使該短生命週期對象使用完以後得不到釋放,從而致使內存泄漏。

所以,弱引用的做用就體現出來了,可使用弱引用來引用短生命週期對象,這樣不會對垃圾回收器回收它形成影響,從而防止內存泄漏。

ThreadLocal中的弱引用

1.爲何ThreadLocalMap使用弱引用存儲ThreadLocal?

假如使用強引用,當ThreadLocal再也不使用須要回收時,發現某個線程中ThreadLocalMap存在該ThreadLocal的強引用,沒法回收,形成內存泄漏。

所以,使用弱引用能夠防止長期存在的線程(一般使用了線程池)致使ThreadLocal沒法回收形成內存泄漏。

2.那一般說的ThreadLocal內存泄漏是如何引發的呢?

咱們注意到Entry對象中,雖然Key(ThreadLocal)是經過弱引用引入的,可是value即變量值自己是經過強引用引入。

這就致使,假如不做任何處理,因爲ThreadLocalMap和線程的生命週期是一致的,當線程資源長期不釋放,即便ThreadLocal自己因爲弱引用機制已經回收掉了,但value仍是駐留在線程的ThreadLocalMap的Entry中。即存在key爲null,但value卻有值的無效Entry。致使內存泄漏。

但實際上,ThreadLocal內部已經爲咱們作了必定的防止內存泄漏的工做。

即以下方法:

/**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        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;
        }

上述方法的做用是擦除某個下標的Entry(置爲null,能夠回收),同時檢測整個Entry[]表中對key爲null的Entry一併擦除,從新調整索引。

該方法,在每次調用ThreadLocal的get、set、remove方法時都會執行,即ThreadLocal內部已經幫咱們作了對key爲null的Entry的清理工做。

可是該工做是有觸發條件的,須要調用相應方法,假如咱們使用完以後不作任何處理是不會觸發的。

總結

  • (強制)在代碼邏輯中使用完ThreadLocal,都要調用remove方法,及時清理。

目前咱們使用多線程都是經過線程池管理的,對於核心線程數以內的線程都是長期駐留池內的。顯式調用remove,一方面是防止內存泄漏,最爲重要的是,不及時清除有可能致使嚴重的業務邏輯問題,產生線上故障(使用了上次未清除的值)。

最佳實踐:在ThreadLocal使用先後都調用remove清理,同時對異常狀況也要在finally中清理。

  • (非規範)對ThreadLocal是否使用全局static修飾的討論。

在某些代碼規範中遇到過這樣一條要求:「儘可能不要使用全局的ThreadLocal」。關於這點有兩種解讀。最初個人解讀是,由於靜態變量的生命週期和類的生命週期是一致的,而類的卸載時機能夠說比較苛刻,這會致使靜態ThreadLocal沒法被垃圾回收,容易出現內存泄漏。另外一個解讀,我諮詢了編寫該規範的對方解釋是,若是流程中改變了變量值,下次複用該流程可能致使獲取到非預期的值。

但實際上,這兩個解讀都是沒必要要的,首先,靜態ThreadLocal資源回收的問題,即便ThreadLocal自己沒法回收,但線程中的Entry是能夠經過remove清理掉的也就不會出現泄漏。第二種解讀,屢次複用值改變的問題,其實在調用remove後也不會出現。

而若是ThreadLocal不加static,則每次其所在類實例化時,都會有重複ThreadLocal建立。這樣即便線程在訪問時不出現錯誤也有資源浪費。

所以,ThreadLocal通常加static修飾,同時要遵循第一條及時清理

我的博客:www.hellolvs.cn

相關文章
相關標籤/搜索