天天看點

Spring事務(二、源碼分析之ThreadLocal)ThreadLocal是什麼ThreadLocal公共方法ThreadLocalMap缺點總結

Thread在管理request作用域的Bean、事務管理、任務排程、AOP等子產品中都有它的身影,是以想了解Spring事務管理的底層技術,ThreadLocal是必須攻克的“山頭堡壘”。

ThreadLocal是什麼

ThreadLocal為解決多線程程式的并發問題提供了一種新的思路,使用這個工具類可以很簡潔地編寫出優美的多線程程式。

ThreadLocal,顧名思義,它不是一個線程,而且儲存線程本地化對象的容器。當運作于多線程環境的某個對象使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程配置設定一個獨立的變量副本。是以沒和線程都可以獨立地改變自己的副本,而不會影響其他線程所對應的副本。

InheritableThreadLocal 繼承于 ThreadLocal,它自動為子線程複制一份從父線程那裡繼承而來的本地變量:在建立子線程時,子線程會接收所有可繼承的線程本地變量的初始值。當必須将本地線程變量自動傳送給所有建立的子線程時,應盡可能地使用InheritableThreadLocal,而非ThreadLocal。

ThreadLocal公共方法

ThreadLocal主要是4個public方法,其他的方法都是輔助這4個方法。

get() - 擷取線程局部變量的目前副本中的值

/**
     * 傳回此線程局部變量的目前線程副本中的值。如果變量沒有目前線程的值,
     * 則首先将其初始化為調用 initialvalue 方法傳回的值。
     * @return 此線程的目前線程的本地值
     */
    public T get() {
        Thread t = Thread.currentThread();
        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();
    }
           

set(T value) - 将此線程局部變量的目前線程副本設定為指定值

/**
     * 将此線程局部變量的目前線程副本設定為指定值。大多數子類将不需要重寫這個方法,
     * 僅僅依靠 initialvalue 方法來設定線程局部變量的值。
     * @param value 要存儲在此線程的目前線程本地副本中的值。
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
           

remove() - 删除此線程局部變量的目前線程值

/**
     * 删除此線程局部變量的目前線程值。如果此線程局部變量随後被目前線程@linkplain讀取,
     * 則通過調用其 initialvalue 方法重新初始化其值,除非其值是 linkplain 由臨時中的目前線程設定。
     * 這可能導緻在目前線程中多次調用 initialvalue 方法。
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
           

withInitial(Supplier<? extends S> supplier) - 建立線程局部變量

/**
     * 建立線程局部變量。變量的初始值通過調用upplier上的get方法來确定。
     * @param<s>線程局部值的類型
     * @參數supplier用于确定初始值的供應商
     * @傳回新的線程局部變量
     * @如果supplier為空,則引發NullPointerException
     * 
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
           

我們可以看到,裡面有一個内部資料結構ThreadLocalMap

ThreadLocalMap

雖然叫ThreadLocalMap,但是其并沒有實作Map接口,其内部是自己實作的一個Entry對象,以及kye,value格式。

我們看下其重要方法:

Entry

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

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

注意

  1. Entry繼承了WeakReference(弱引用)。
  2. 其key是寫死的ThreadLocal類型。但Value可以為任意類型。
  3. Entry的kye是弱引用類型的,Value并非弱引用。

ThreadLocalMap其他參數

/**
         * 初始容量
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 表,大小必須是2的幂。
         */
        private Entry[] table;

        /**
         * 表大小
         */
        private int size = 0;

        /**
         * 要調整大小的下一個大小值。
         */
        private int threshold; // Default to 0

        /**
         * 門檻值,設定最壞是長度的2/3。
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
           

作為一個map,肯定避免不了hash沖突以及擴容問題。那麼ThreadLocalMap是如何實作的。

hash沖突

/**
         * 增加
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 減少
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
           

我們可以看到ThreadLocalMap使用的是最簡單,步長加1或減1,尋找下一個相鄰的位置。也是線性探測。

線性探測:

根據初始key的hashcode值确定元素在table數組中的位置,如果發現這個位置上已經有其他key值的元素被占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。

上面介紹ThreadLocal 以及 ThreadLocalMap就暫時介紹完了。

我們下面說下ThreadLocalMap缺點:

ThreadLocalMap缺點

  1. 解決hash沖突效率低

因為是使用的是線性探測法,步長+1或者-1,如果有大量不同的ThreadLocal對象放入map中時發送沖突,或者發生二次沖突,則效率很低。

  1. 弱引用記憶體洩露問題

    整理自https://www.jianshu.com/p/a1cd61fa22da

再說問題産生原因和解決辦法前,我們先說下為什麼要使用弱引用:

為什麼要使用弱引用

從表面上看,發生記憶體洩漏,是因為Key使用了弱引用類型。但其實是因為整個Entry的key為null後,沒有主動清除value導緻。很多文章大多分析ThreadLocal使用了弱引用會導緻記憶體洩漏,但為什麼使用弱引用而不是強引用?

官方文檔的說法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

為了處理非常大和生命周期非常長的線程,哈希表使用弱引用作為 key。

下面我們分兩種情況讨論:

  • key 使用強引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動删除,ThreadLocal不會被回收,導緻Entry記憶體洩漏。
  • key 使用弱引用:引用的ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動删除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。

比較兩種情況,我們可以發現:由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動删除對應key,都會導緻記憶體洩漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會記憶體洩漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。

是以,ThreadLocal記憶體洩漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動删除對應key的value就會導緻記憶體洩漏,而不是因為弱引用。

産生洩露原因

ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,當ThreadLocal沒有外部強引用來引用它的時候,ThreadLocal會在下次JVM垃圾收集時被回收。

這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。是以如果目前線程的生命周期很長,一直存在,那麼其内部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鍊的關系一直存在:Thread --> ThreadLocalMap–>Entry–>Value,這條強引用鍊會導緻Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成記憶體洩漏。

解決辦法

但是JVM團隊已經考慮到這樣的情況,并做了一些措施來保證ThreadLocal盡量不會記憶體洩漏:在ThreadLocal的get()、set()、remove()方法調用的時候會清除掉線程ThreadLocalMap中所有Entry中Key為null的Value,并将整個Entry設定為null,利于下次記憶體回收。

在get()方法中調用map.getEntry(this)時,其内部會判斷key是否為null,繼續看map.getEntry(this)源碼:

getEntry(ThreadLocal<?> key)

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 判斷Entry是否為空,以及key是否為null
            if (e != null && e.get() == key)
                return e;
                // key為空調用getEntryAfterMiss()
            else
                return getEntryAfterMiss(key, i, e);
        }
           

getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)

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;
                    // 如果key == null,調用expungeStaleEntry
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

           

expungeStaleEntry(int staleSlot)

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

            // expunge entry at staleSlot 
            // 設定value = null,删除value,便于下次回收。
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // 循環檢查,判斷是否有key == null 的存在,如果有,一并将其value 設定為 null,友善回收
            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;
        }
           

經過上面的步驟,其實也不能保證ThreadLocal不會發生記憶體洩漏,例如:

  • 使用static的ThreadLocal,延長了ThreadLocal的生命周期,可能導緻的記憶體洩漏。
  • 配置設定使用了ThreadLocal又不再調用get()、set()、remove()方法,那麼就會導緻記憶體洩漏。

總結

綜合上面的分析,我們可以了解ThreadLocal記憶體洩漏的前因後果,那麼怎麼避免記憶體洩漏呢?

每次使用完ThreadLocal,都調用它的remove()方法,清除資料。

在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是記憶體洩漏的問題,更嚴重的是可能導緻業務邏輯出現問題。是以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。

我們可能還聽說過線程同步機制。它也是為了解決多線程中相同變量的通路沖突問題。那麼二者相比有什麼不同呢。

  • 線程同步機制:通過對象的鎖機制保證同一時間隻有一個線程通路變量。此時該變量是線程共享的,需要使用程式分析什麼時候濟甯讀/寫、什麼時候鎖定、什麼時候釋放等問題,程式更新和編寫難度大。采用了“時間換空間”的方式。
  • ThreadLocal:為每個線程提供了一個獨立的變量副本,進而隔離了多個線程對通路資料的沖突。因為每個線程都擁有自己的變量副本,是以沒必要對該變量進行同步。可以把不安全的變量封裝進ThreadLocal。采用了“空間換時間”的方式。