Spring與線程安全
Spring作為一個(gè)IOC/DI容器见转,幫助我們管理了許許多多的“bean”命雀。但其實(shí),Spring并沒有保證這些對(duì)象的線程安全斩箫,需要由開發(fā)者自己編寫解決線程安全問題的代碼吏砂。
Spring對(duì)每個(gè)bean提供了一個(gè)scope屬性來表示該bean的作用域。它是bean的生命周期乘客。例如狐血,一個(gè)scope為singleton的bean,在第一次被注入時(shí)易核,會(huì)創(chuàng)建為一個(gè)單例對(duì)象匈织,該對(duì)象會(huì)一直被復(fù)用到應(yīng)用結(jié)束。
singleton:默認(rèn)的scope,每個(gè)scope為singleton的bean都會(huì)被定義為一個(gè)單例對(duì)象缀匕,該對(duì)象的生命周期是與Spring IOC容器一致的(但在第一次被注入時(shí)才會(huì)創(chuàng)建)纳决。
prototype:bean被定義為在每次注入時(shí)都會(huì)創(chuàng)建一個(gè)新的對(duì)象。
request:bean被定義為在每個(gè)HTTP請(qǐng)求中創(chuàng)建一個(gè)單例對(duì)象弦追,也就是說在單個(gè)請(qǐng)求中都會(huì)復(fù)用這一個(gè)單例對(duì)象岳链。
session:bean被定義為在一個(gè)session的生命周期內(nèi)創(chuàng)建一個(gè)單例對(duì)象。
application:bean被定義為在ServletContext的生命周期中復(fù)用一個(gè)單例對(duì)象劲件。
websocket:bean被定義為在websocket的生命周期中復(fù)用一個(gè)單例對(duì)象掸哑。
我們交由Spring管理的大多數(shù)對(duì)象其實(shí)都是一些無狀態(tài)的對(duì)象,這種不會(huì)因?yàn)槎嗑€程而導(dǎo)致狀態(tài)被破壞的對(duì)象很適合Spring的默認(rèn)scope零远,每個(gè)單例的無狀態(tài)對(duì)象都是線程安全的(也可以說只要是無狀態(tài)的對(duì)象苗分,不管單例多例都是線程安全的,不過單例畢竟節(jié)省了不斷創(chuàng)建對(duì)象與GC的開銷)牵辣。
無狀態(tài)的對(duì)象即是自身沒有狀態(tài)的對(duì)象摔癣,自然也就不會(huì)因?yàn)槎鄠€(gè)線程的交替調(diào)度而破壞自身狀態(tài)導(dǎo)致線程安全問題。無狀態(tài)對(duì)象包括我們經(jīng)常使用的DO纬向、DTO择浊、VO這些只作為數(shù)據(jù)的實(shí)體模型的貧血對(duì)象,還有Service逾条、DAO和Controller琢岩,這些對(duì)象并沒有自己的狀態(tài),它們只是用來執(zhí)行某些操作的师脂。例如担孔,每個(gè)DAO提供的函數(shù)都只是對(duì)數(shù)據(jù)庫的CRUD,而且每個(gè)數(shù)據(jù)庫Connection都作為函數(shù)的局部變量(局部變量是在用戶棧中的吃警,而且用戶棧本身就是線程私有的內(nèi)存區(qū)域糕篇,所以不存在線程安全問題),用完即關(guān)(或交還給連接池)酌心。
有人可能會(huì)認(rèn)為拌消,我使用request作用域不就可以避免每個(gè)請(qǐng)求之間的安全問題了嗎?這是完全錯(cuò)誤的安券,因?yàn)镃ontroller默認(rèn)是單例的墩崩,一個(gè)HTTP請(qǐng)求是會(huì)被多個(gè)線程執(zhí)行的,這就又回到了線程的安全問題完疫。當(dāng)然泰鸡,你也可以把Controller的scope改成prototype债蓝,實(shí)際上Struts2就是這么做的壳鹤,但有一點(diǎn)要注意,Spring MVC對(duì)請(qǐng)求的攔截粒度是基于每個(gè)方法的饰迹,而Struts2是基于每個(gè)類的芳誓,所以把Controller設(shè)為多例將會(huì)頻繁的創(chuàng)建與回收對(duì)象余舶,嚴(yán)重影響到了性能。
通過閱讀上文其實(shí)已經(jīng)說的很清楚了锹淌,Spring根本就沒有對(duì)bean的多線程安全問題做出任何保證與措施匿值。對(duì)于每個(gè)bean的線程安全問題,根本原因是每個(gè)bean自身的設(shè)計(jì)赂摆。不要在bean中聲明任何有狀態(tài)的實(shí)例變量或類變量挟憔,如果必須如此,那么就使用ThreadLocal把變量變?yōu)榫€程私有的烟号,如果bean的實(shí)例變量或類變量需要在多個(gè)線程之間共享绊谭,那么就只能使用synchronized、lock汪拥、CAS等這些實(shí)現(xiàn)線程同步的方法了达传。
下面將通過解析ThreadLocal的源碼來了解它的實(shí)現(xiàn)與作用,ThreadLocal是一個(gè)很好用的工具類迫筑,它在某些情況下解決了線程安全問題(在變量不需要被多個(gè)線程共享時(shí))宪赶。
本文作者為SylvanasSun(sylvanas.sun@gmail.com),首發(fā)于SylvanasSun’s Blog脯燃。
原文鏈接:https://sylvanassun.github.io/2017/11/06/2017-11-06-spring_and_thread-safe/
(轉(zhuǎn)載請(qǐng)務(wù)必保留本段聲明搂妻,并且保留超鏈接熊咽。)
ThreadLocal
ThreadLocal是一個(gè)為線程提供線程局部變量的工具類沸版。它的思想也十分簡單赴穗,就是為線程提供一個(gè)線程私有的變量副本剧浸,這樣多個(gè)線程都可以隨意更改自己線程局部的變量诅愚,不會(huì)影響到其他線程垃瞧。不過需要注意的是酝锅,ThreadLocal提供的只是一個(gè)淺拷貝到涂,如果變量是一個(gè)引用類型懈糯,那么就要考慮它內(nèi)部的狀態(tài)是否會(huì)被改變涤妒,想要解決這個(gè)問題可以通過重寫ThreadLocal的initialValue()函數(shù)來自己實(shí)現(xiàn)深拷貝,建議在使用ThreadLocal時(shí)一開始就重寫該函數(shù)赚哗。
ThreadLocal與像synchronized這樣的鎖機(jī)制是不同的她紫。首先,它們的應(yīng)用場景與實(shí)現(xiàn)思路就不一樣屿储,鎖更強(qiáng)調(diào)的是如何同步多個(gè)線程去正確地共享一個(gè)變量贿讹,ThreadLocal則是為了解決同一個(gè)變量如何不被多個(gè)線程共享。從性能開銷的角度上來講够掠,如果鎖機(jī)制是用時(shí)間換空間的話民褂,那么ThreadLocal就是用空間換時(shí)間。
ThreadLocal中含有一個(gè)叫做ThreadLocalMap的內(nèi)部類,該類為一個(gè)采用線性探測(cè)法實(shí)現(xiàn)的HashMap赊堪。它的key為ThreadLocal對(duì)象而且還使用了WeakReference面殖,ThreadLocalMap正是用來存儲(chǔ)變量副本的。
/**
* 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中只含有三個(gè)成員變量哭廉,這三個(gè)變量都是與ThreadLocalMap的hash策略相關(guān)的脊僚。
/**
* 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);
}
唯一的實(shí)例變量threadLocalHashCode是用來進(jìn)行尋址的hashcode,它由函數(shù)nextHashCode()生成遵绰,該函數(shù)簡單地通過一個(gè)增量HASH_INCREMENT來生成hashcode辽幌。至于為什么這個(gè)增量為0x61c88647,主要是因?yàn)門hreadLocalMap的初始大小為16椿访,每次擴(kuò)容都會(huì)為原來的2倍舶衬,這樣它的容量永遠(yuǎn)為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);
}
要獲得當(dāng)前線程私有的變量副本需要調(diào)用get()函數(shù)。首先梁剔,它會(huì)調(diào)用getMap()函數(shù)去獲得當(dāng)前線程的ThreadLocalMap虽画,這個(gè)函數(shù)需要接收當(dāng)前線程的實(shí)例作為參數(shù)。如果得到的ThreadLocalMap為null荣病,那么就去調(diào)用setInitialValue()函數(shù)來進(jìn)行初始化码撰,如果不為null,就通過map來獲得變量副本并返回个盆。
setInitialValue()函數(shù)會(huì)去先調(diào)用initialValue()函數(shù)來生成初始值脖岛,該函數(shù)默認(rèn)返回null,我們可以通過重寫這個(gè)函數(shù)來返回我們想要在ThreadLocal中維護(hù)的變量颊亮。之后柴梆,去調(diào)用getMap()函數(shù)獲得ThreadLocalMap,如果該map已經(jīng)存在终惑,那么就用新獲得value去覆蓋舊值绍在,否則就調(diào)用createMap()函數(shù)來創(chuàng)建新的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()函數(shù)要比get()的實(shí)現(xiàn)還要簡單雹有,都只是通過getMap()來獲得ThreadLocalMap然后對(duì)其進(jìn)行操作偿渡。
/**
* 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()函數(shù)與createMap()函數(shù)的實(shí)現(xiàn)也十分簡單,但是通過觀察這兩個(gè)函數(shù)可以發(fā)現(xiàn)一個(gè)秘密: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;
仔細(xì)想想其實(shí)就能夠理解這種設(shè)計(jì)的思想溜宽。有一種普遍的方法是通過一個(gè)全局的線程安全的Map來存儲(chǔ)各個(gè)線程的變量副本,但是這種做法已經(jīng)完全違背了ThreadLocal的本意质帅,設(shè)計(jì)ThreadLocal的初衷就是為了避免多個(gè)線程去并發(fā)訪問同一個(gè)對(duì)象适揉,盡管它是線程安全的合武。而在每個(gè)Thread中存放與它關(guān)聯(lián)的ThreadLocalMap是完全符合ThreadLocal的思想的,當(dāng)想要對(duì)線程局部變量進(jìn)行操作時(shí)涡扼,只需要把Thread作為key來獲得Thread中的ThreadLocalMap即可。這種設(shè)計(jì)相比采用一個(gè)全局Map的方法會(huì)多占用很多內(nèi)存空間盟庞,但也因此不需要額外的采取鎖等線程同步方法而節(jié)省了時(shí)間上的消耗吃沪。
ThreadLocal中的內(nèi)存泄漏
我們要考慮一種會(huì)發(fā)生內(nèi)存泄漏的情況,如果ThreadLocal被設(shè)置為null后什猖,而且沒有任何強(qiáng)引用指向它票彪,根據(jù)垃圾回收的可達(dá)性分析算法,ThreadLocal將會(huì)被回收不狮。這樣一來降铸,ThreadLocalMap中就會(huì)含有key為null的Entry,而且ThreadLocalMap是在Thread中的摇零,只要線程遲遲不結(jié)束推掸,這些無法訪問到的value會(huì)形成內(nèi)存泄漏。為了解決這個(gè)問題驻仅,ThreadLocalMap中的getEntry()谅畅、set()和remove()函數(shù)都會(huì)清理key為null的Entry,以下面的getEntry()函數(shù)的源碼為例噪服。
/**
* 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;
}
在上文中我們發(fā)現(xiàn)了ThreadLocalMap的key是一個(gè)弱引用毡泻,那么為什么使用弱引用呢?使用強(qiáng)引用key與弱引用key的差別如下:
強(qiáng)引用key:ThreadLocal被設(shè)置為null粘优,由于ThreadLocalMap持有ThreadLocal的強(qiáng)引用仇味,如果不手動(dòng)刪除,那么ThreadLocal將不會(huì)回收雹顺,產(chǎn)生內(nèi)存泄漏丹墨。
弱引用key:ThreadLocal被設(shè)置為null,由于ThreadLocalMap持有ThreadLocal的弱引用嬉愧,即便不手動(dòng)刪除带到,ThreadLocal仍會(huì)被回收,ThreadLocalMap在之后調(diào)用set()英染、getEntry()和remove()函數(shù)時(shí)會(huì)清除所有key為null的Entry揽惹。
但要注意的是,ThreadLocalMap僅僅含有這些被動(dòng)措施來補(bǔ)救內(nèi)存泄漏問題四康。如果你在之后沒有調(diào)用ThreadLocalMap的set()搪搏、getEntry()和remove()函數(shù)的話,那么仍然會(huì)存在內(nèi)存泄漏問題闪金。
在使用線程池的情況下疯溺,如果不及時(shí)進(jìn)行清理论颅,內(nèi)存泄漏問題事小,甚至還會(huì)產(chǎn)生程序邏輯上的問題囱嫩。所以恃疯,為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣墨闲,在每次使用完ThreadLocal后都要調(diào)用remove()來清理無用的Entry今妄。