聊一聊Spring中的線程安全性

Spring與線程安全


Spring做爲一個IOC/DI容器,幫助咱們管理了許許多多的「bean」。但其實,Spring並無保證這些對象的線程安全,須要由開發者本身編寫解決線程安全問題的代碼。html

Spring對每一個bean提供了一個scope屬性來表示該bean的做用域。它是bean的生命週期。例如,一個scope爲singleton的bean,在第一次被注入時,會建立爲一個單例對象,該對象會一直被複用到應用結束。java

  • singleton:默認的scope,每一個scope爲singleton的bean都會被定義爲一個單例對象,該對象的生命週期是與Spring IOC容器一致的(但在第一次被注入時纔會建立)。git

  • prototype:bean被定義爲在每次注入時都會建立一個新的對象。github

  • request:bean被定義爲在每一個HTTP請求中建立一個單例對象,也就是說在單個請求中都會複用這一個單例對象。web

  • session:bean被定義爲在一個session的生命週期內建立一個單例對象。算法

  • application:bean被定義爲在ServletContext的生命週期中複用一個單例對象。spring

  • websocket:bean被定義爲在websocket的生命週期中複用一個單例對象。數據庫

咱們交由Spring管理的大多數對象其實都是一些無狀態的對象,這種不會由於多線程而致使狀態被破壞的對象很適合Spring的默認scope,每一個單例的無狀態對象都是線程安全的(也能夠說只要是無狀態的對象,無論單例多例都是線程安全的,不過單例畢竟節省了不斷建立對象與GC的開銷)。安全

無狀態的對象便是自身沒有狀態的對象,天然也就不會由於多個線程的交替調度而破壞自身狀態致使線程安全問題。無狀態對象包括咱們常用的DO、DTO、VO這些只做爲數據的實體模型的貧血對象,還有Service、DAO和Controller,這些對象並無本身的狀態,它們只是用來執行某些操做的。例如,每一個DAO提供的函數都只是對數據庫的CRUD,並且每一個數據庫Connection都做爲函數的局部變量(局部變量是在用戶棧中的,並且用戶棧自己就是線程私有的內存區域,因此不存在線程安全問題),用完即關(或交還給鏈接池)。websocket

有人可能會認爲,我使用request做用域不就能夠避免每一個請求之間的安全問題了嗎?這是徹底錯誤的,由於Controller默認是單例的,一個HTTP請求是會被多個線程執行的,這就又回到了線程的安全問題。固然,你也能夠把Controller的scope改爲prototype,實際上Struts2就是這麼作的,但有一點要注意,Spring MVC對請求的攔截粒度是基於每一個方法的,而Struts2是基於每一個類的,因此把Controller設爲多例將會頻繁的建立與回收對象,嚴重影響到了性能。

經過閱讀上文其實已經說的很清楚了,Spring根本就沒有對bean的多線程安全問題作出任何保證與措施。對於每一個bean的線程安全問題,根本緣由是每一個bean自身的設計。不要在bean中聲明任何有狀態的實例變量或類變量,若是必須如此,那麼就使用ThreadLocal把變量變爲線程私有的,若是bean的實例變量或類變量須要在多個線程之間共享,那麼就只能使用synchronized、lock、CAS等這些實現線程同步的方法了。

下面將經過解析ThreadLocal的源碼來了解它的實現與做用,ThreadLocal是一個很好用的工具類,它在某些狀況下解決了線程安全問題(在變量不須要被多個線程共享時)。

本文做者爲SylvanasSun(sylvanas.sun@gmail.com),首發於SylvanasSun’s Blog
原文連接:sylvanassun.github.io/2017/11/06/…
(轉載請務必保留本段聲明,而且保留超連接。)

ThreadLocal


ThreadLocal是一個爲線程提供線程局部變量的工具類。它的思想也十分簡單,就是爲線程提供一個線程私有的變量副本,這樣多個線程均可以隨意更改本身線程局部的變量,不會影響到其餘線程。不過須要注意的是,ThreadLocal提供的只是一個淺拷貝,若是變量是一個引用類型,那麼就要考慮它內部的狀態是否會被改變,想要解決這個問題能夠經過重寫ThreadLocal的initialValue()函數來本身實現深拷貝,建議在使用ThreadLocal時一開始就重寫該函數。

ThreadLocal與像synchronized這樣的鎖機制是不一樣的。首先,它們的應用場景與實現思路就不同,鎖更強調的是如何同步多個線程去正確地共享一個變量,ThreadLocal則是爲了解決同一個變量如何不被多個線程共享。從性能開銷的角度上來說,若是鎖機制是用時間換空間的話,那麼ThreadLocal就是用空間換時間。

ThreadLocal中含有一個叫作ThreadLocalMap的內部類,該類爲一個採用線性探測法實現的HashMap。它的key爲ThreadLocal對象並且還使用了WeakReference,ThreadLocalMap正是用來存儲變量副本的。

/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */
    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;
            }
        }
        ....
    }複製代碼

ThreadLocal中只含有三個成員變量,這三個變量都是與ThreadLocalMap的hash策略相關的。

/** * ThreadLocals rely on per-thread linear-probe hash maps attached * to each thread (Thread.threadLocals and * inheritableThreadLocals). The ThreadLocal objects act as keys, * searched via threadLocalHashCode. This is a custom hash code * (useful only within ThreadLocalMaps) that eliminates collisions * in the common case where consecutively constructed ThreadLocals * are used by the same threads, while remaining well-behaved in * less common cases. */
    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);
    }複製代碼

惟一的實例變量threadLocalHashCode是用來進行尋址的hashcode,它由函數nextHashCode()生成,該函數簡單地經過一個增量HASH_INCREMENT來生成hashcode。至於爲何這個增量爲0x61c88647,主要是由於ThreadLocalMap的初始大小爲16,每次擴容都會爲原來的2倍,這樣它的容量永遠爲2的n次方,該增量選爲0x61c88647也是爲了儘量均勻地分佈,減小碰撞衝突。

/** * The initial capacity -- MUST be a power of two. */
        private static final int INITIAL_CAPACITY = 16;    

        /** * Construct a new map initially containing (firstKey, firstValue). * ThreadLocalMaps are constructed lazily, so we only create * one when we have at least one entry to put in it. */
        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);
        }複製代碼

要得到當前線程私有的變量副本須要調用get()函數。首先,它會調用getMap()函數去得到當前線程的ThreadLocalMap,這個函數須要接收當前線程的實例做爲參數。若是獲得的ThreadLocalMap爲null,那麼就去調用setInitialValue()函數來進行初始化,若是不爲null,就經過map來得到變量副本並返回。

setInitialValue()函數會去先調用initialValue()函數來生成初始值,該函數默認返回null,咱們能夠經過重寫這個函數來返回咱們想要在ThreadLocal中維護的變量。以後,去調用getMap()函數得到ThreadLocalMap,若是該map已經存在,那麼就用新得到value去覆蓋舊值,不然就調用createMap()函數來建立新的map。

/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */
    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();
    }

    /** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }複製代碼

ThreadLocal的set()與remove()函數要比get()的實現還要簡單,都只是經過getMap()來得到ThreadLocalMap而後對其進行操做。

/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /** * Removes the current thread's value for this thread-local * variable. If this thread-local variable is subsequently * {@linkplain #get read} by the current thread, its value will be * reinitialized by invoking its {@link #initialValue} method, * unless its value is {@linkplain #set set} by the current thread * in the interim. This may result in multiple invocations of the * {@code initialValue} method in the current thread. * * @since 1.5 */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }複製代碼

getMap()函數與createMap()函數的實現也十分簡單,可是經過觀察這兩個函數能夠發現一個祕密:ThreadLocalMap是存放在Thread中的。

/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    // Thread中的源碼

    /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;複製代碼

仔細想一想其實就可以理解這種設計的思想。有一種廣泛的方法是經過一個全局的線程安全的Map來存儲各個線程的變量副本,可是這種作法已經徹底違背了ThreadLocal的本意,設計ThreadLocal的初衷就是爲了不多個線程去併發訪問同一個對象,儘管它是線程安全的。而在每一個Thread中存放與它關聯的ThreadLocalMap是徹底符合ThreadLocal的思想的,當想要對線程局部變量進行操做時,只須要把Thread做爲key來得到Thread中的ThreadLocalMap便可。這種設計相比採用一個全局Map的方法會多佔用不少內存空間,但也所以不須要額外的採起鎖等線程同步方法而節省了時間上的消耗。

ThreadLocal中的內存泄漏


咱們要考慮一種會發生內存泄漏的狀況,若是ThreadLocal被設置爲null後,並且沒有任何強引用指向它,根據垃圾回收的可達性分析算法,ThreadLocal將會被回收。這樣一來,ThreadLocalMap中就會含有key爲null的Entry,並且ThreadLocalMap是在Thread中的,只要線程遲遲不結束,這些沒法訪問到的value會造成內存泄漏。爲了解決這個問題,ThreadLocalMap中的getEntry()、set()和remove()函數都會清理key爲null的Entry,如下面的getEntry()函數的源碼爲例。

/** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for direct hits, in part * by making this method readily inlinable. * * @param key the thread local object * @return the entry associated with key, or null if no such */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /** * Version of getEntry method for use when key is not found in * its direct hash slot. * * @param key the thread local object * @param i the table index for key's hash code * @param e the entry at table[i] * @return the entry associated with key, or null if no such */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            // 清理key爲null的Entry
            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;
        }複製代碼

在上文中咱們發現了ThreadLocalMap的key是一個弱引用,那麼爲何使用弱引用呢?使用強引用key與弱引用key的差異以下:

  • 強引用key:ThreadLocal被設置爲null,因爲ThreadLocalMap持有ThreadLocal的強引用,若是不手動刪除,那麼ThreadLocal將不會回收,產生內存泄漏。

  • 弱引用key:ThreadLocal被設置爲null,因爲ThreadLocalMap持有ThreadLocal的弱引用,即使不手動刪除,ThreadLocal仍會被回收,ThreadLocalMap在以後調用set()、getEntry()和remove()函數時會清除全部key爲null的Entry。

但要注意的是,ThreadLocalMap僅僅含有這些被動措施來補救內存泄漏問題。若是你在以後沒有調用ThreadLocalMap的set()、getEntry()和remove()函數的話,那麼仍然會存在內存泄漏問題。

在使用線程池的狀況下,若是不及時進行清理,內存泄漏問題事小,甚至還會產生程序邏輯上的問題。因此,爲了安全地使用ThreadLocal,必需要像每次使用完鎖就解鎖同樣,在每次使用完ThreadLocal後都要調用remove()來清理無用的Entry。

參考文獻


相關文章
相關標籤/搜索