詳談ThreadLocal-單例模式下高并發(fā)線程安全
ThreadLocal-單例模式下高并發(fā)線程安全
在多例的情況下,每個(gè)對象在堆中聲明內(nèi)存空間,多線程對應(yīng)的Java棧中的句柄或指針指向堆中不同的對象,對象各自變量的變更只會(huì)印象到對應(yīng)的棧,也就是對應(yīng)的線程中,不會(huì)影響到其它線程。所以多例的情況下不需要考慮線程安全的問題,因?yàn)橐欢ㄊ前踩摹?/p>
而在單例的情況下卻完全不一樣了,在堆中只有一個(gè)對象,多線程對應(yīng)的Java棧中的句柄或指針指向同一個(gè)對象,方法的參數(shù)變量和方法內(nèi)變量是線程安全的,因?yàn)槊繄?zhí)行一個(gè)方法,都會(huì)在獨(dú)立的空間創(chuàng)建局部變量,它不是共享的資源。但是成員變量只有一份,所有指向堆中該對象的句柄或指針都可以隨時(shí)修改和讀取它,所以是非線程安全的。
為了解決線程安全的問題,我們有3個(gè)思路:
- 第一每個(gè)線程獨(dú)享自己的操作對象,也就是多例,多例勢必會(huì)帶來堆內(nèi)存占用、頻繁GC、對象初始化性能開銷等待等一些列問題。
- 第二單例模式枷鎖,典型的案例是HashTable和HashMap,對讀取和變更的操作用synchronized限制起來,保證同一時(shí)間只有一個(gè)線程可以操作該對象。雖然解決了內(nèi)存、回收、構(gòu)造、初始化等問題,但是勢必會(huì)因?yàn)殒i競爭帶來高并發(fā)下性能的下降。
- 第三個(gè)思路就是今天重點(diǎn)推出的ThreadLocal。單例模式下通過某種機(jī)制維護(hù)成員變量不同線程的版本。
- 假設(shè)三個(gè)人想從鏡子中看自己,第一個(gè)方案就是每人發(fā)一個(gè)鏡子互不干擾,第二個(gè)方案就是只有一個(gè)鏡子,一個(gè)人站在鏡子前其他人要排隊(duì)等候,第三個(gè)方案就是我這里發(fā)明了一種“魔鏡”,所有人站在鏡子前可以并且只能看到自己?。?!
主程序:
public static void main(String[] args) { //Mirror是個(gè)單例的,只構(gòu)建了一個(gè)對象 Mirror mirror = new Mirror("魔鏡"); //三個(gè)線程都在用這面鏡子 MirrorThread thread1 = new MirrorThread(mirror,"張三"); MirrorThread thread2 = new MirrorThread(mirror,"李四"); MirrorThread thread3 = new MirrorThread(mirror,"王二"); thread1.start(); thread2.start(); thread3.start(); }
很好理解,創(chuàng)建了一面鏡子,3個(gè)人一起照鏡子。
MirrorThread:
public class MirrorThread extends Thread{ private Mirror mirror; private String threadName; public MirrorThread(Mirror mirror, String threadName){ this.mirror = mirror; this.threadName = threadName; } //照鏡子 public String lookMirror() { return threadName+" looks like "+ mirror.getNowLookLike().get(); } //化妝 public void makeup(String makeupString) { mirror.getNowLookLike().set(makeupString); } @Override public void run() { int i = 1;//閾值 while(i<5) { try { long nowFace = (long)(Math.random()*5000); sleep(nowFace); StringBuffer sb = new StringBuffer(); sb.append("第"+i+"輪從"); sb.append(lookMirror()); makeup(String.valueOf(nowFace)); sb.append("變?yōu)?); sb.append(lookMirror()); System.out.println(sb); } catch (InterruptedException e) { e.printStackTrace(); } i++; } } }
也很好理解,就是不斷的更新自己的外貌同時(shí)從鏡子里讀取自己的外貌。
重點(diǎn)是Mirror:
public class Mirror { private String mirrorName; //每個(gè)人要看到自己的樣子,所以這里要用ThreadLocal private ThreadLocal<String> nowLookLike; public Mirror(String mirrorName){ this.mirrorName=mirrorName; nowLookLike = new ThreadLocal<String>(); } public String getMirrorName() { return mirrorName; } public ThreadLocal<String> getNowLookLike() { return nowLookLike; } }
對每個(gè)人長的樣子用ThreadLocal類型來表示。
先看測試結(jié)果:
第1輪從張三 looks like null變?yōu)閺埲?looks like 3008
第2輪從張三 looks like 3008變?yōu)閺埲?looks like 490
第1輪從王二 looks like null變?yōu)橥醵?looks like 3982
第1輪從李四 looks like null變?yōu)槔钏?looks like 4390
第2輪從王二 looks like 3982變?yōu)橥醵?looks like 1415
第2輪從李四 looks like 4390變?yōu)槔钏?looks like 1255
第3輪從王二 looks like 1415變?yōu)橥醵?looks like 758
第3輪從張三 looks like 490變?yōu)閺埲?looks like 2746
第3輪從李四 looks like 1255變?yōu)槔钏?looks like 845
第4輪從李四 looks like 845變?yōu)槔钏?looks like 1123
第4輪從張三 looks like 2746變?yōu)閺埲?looks like 2126
第4輪從王二 looks like 758變?yōu)橥醵?looks like 4516
OK,一面鏡子所有人一起照,而且每個(gè)人都只能看的到自己的變化,這就達(dá)成了單例線程安全的目的。
我們來細(xì)看下它是怎么實(shí)現(xiàn)的。
先來看Thread:
Thread中維護(hù)了一個(gè)ThreadLocal.ThreadLocalMapthreadLocals = null; ThreadLocalMap這個(gè)Map的key是ThreadLocal,value是維護(hù)的成員變量?,F(xiàn)在的跟蹤鏈?zhǔn)荰hread->ThreadLocalMap-><ThreadLocal,Object>,那么我們只要搞明白Thread怎么跟ThreadLocal關(guān)聯(lián)的,從線程里找到自己關(guān)心的成員變量的快照這條線就通了。
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); }
再來看ThreadLocal:它里面核心方法兩個(gè)get()和set(T)
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(); }
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
方法里通過Thread.currentThread()的方法得到當(dāng)前線程,然后做為key存儲(chǔ)到當(dāng)前線程對象的threadLocals中,也就是TreadLocalMap中。
OK,這樣整個(gè)關(guān)系鏈已經(jīng)建立,真正要去訪問的成員變量在一個(gè)map中,key是線程號(hào),值是屬于該線程的快照。
ThreadLocal里還有map的創(chuàng)建createMap(t, value)、取值時(shí)對象的初始值setInitialValue()、線程結(jié)束時(shí)對象的釋放remove()等細(xì)節(jié),有興趣的可以繼續(xù)跟進(jìn)了解下。
ThreadLocal應(yīng)用其實(shí)很多,例如Spring容器中實(shí)例默認(rèn)是單例的,transactionManager也一樣,那么事務(wù)在處理時(shí)單例的manager是如何控制每個(gè)線程的事務(wù)要如何處理呢,這里面就應(yīng)用了大量的ThreadLocal。
多線程中的ThreadLocal
1.ThreadLocal概述
多線程的并發(fā)問題主要存在于多個(gè)線程對于同一個(gè)變量進(jìn)行修改產(chǎn)生的數(shù)據(jù)不一致的問題,同一個(gè)變量指的值同一個(gè)對象的成員變量或者是同一個(gè)類的靜態(tài)變量。之前我們常聽過盡量不要使用靜態(tài)變量,會(huì)引起并發(fā)問題,那么隨著Spring框架的深入人心,單例中的成員變量也出現(xiàn)了多線程并發(fā)問題。Struts2接受參數(shù)采用成員變量自動(dòng)封裝,為此在Spring的配置采用多例模式,而SpringMVC將Spring的容器化發(fā)揮到極致,將接受的參數(shù)放到了注解和方法的參數(shù)中,從而避免了單例出現(xiàn)的線程問題。今天,我們討論的是JDK從1.2就出現(xiàn)的一個(gè)并發(fā)工具類ThreadLocal,他除了加鎖這種同步方式之外的另一種保證一種規(guī)避多線程訪問出現(xiàn)線程不安全的方法,當(dāng)我們在創(chuàng)建一個(gè)變量后,如果每個(gè)線程對其進(jìn)行訪問的時(shí)候訪問的都是線程自己的變量這樣就不會(huì)存在線程不安全問題。我們先看一下官方是怎么解釋這個(gè)變量的?
大致意思是:此類提供了局部變量表。這些變量與普通變量不同不同之處是,每一個(gè)通過get或者set方法訪問一個(gè)線程都是他自己的,將變量的副本獨(dú)立初始化。ThreadLocal實(shí)例通常作用于希望將狀態(tài)與線程關(guān)聯(lián)的類中的私有靜態(tài)字段(例如,用戶ID或交易ID)。
只要線程是活動(dòng)的并且可以訪問{@code ThreadLocal}實(shí)例, 每個(gè)線程都會(huì)對其線程局部變量的副本保留隱式引用。 線程消失后,其線程本地實(shí)例的所有副本都將進(jìn)行垃圾回收(除非存在對這些副本的其他引用)。也就是說,如果創(chuàng)建一個(gè)ThreadLocal變量,那么訪問這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的一個(gè)副本,在實(shí)際多線程操作的時(shí)候,操作的是自己本地內(nèi)存中的變量,從而規(guī)避了線程安全問題。而每個(gè)線程的副本全部放到ThreadLocalMap中。
2. ThreadLocal簡單實(shí)用
public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocal<Double> threadLocal = new ThreadLocal(); private Double variable; @Override public void run() { threadLocal.set(Math.floor(Math.random() * 100D)); variable = Math.floor(Math.random() * 100D); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("ThreadValue==>"+threadLocal.get()); System.out.println("Variable==>"+variable); } } public static void main(String[] args) { MyRunnable sharedRunnableInstance = new MyRunnable(); Thread thread1 = new Thread(sharedRunnableInstance); Thread thread2 = new Thread(sharedRunnableInstance); thread1.start(); thread2.start(); } }
通過上面的例子,我們發(fā)現(xiàn)將Double放入ThreadLocal中,不會(huì)出現(xiàn)多線程并發(fā)問題,而成員變量variable卻發(fā)生了多線程并發(fā)問題。
3.ThreadLocal的內(nèi)部原理
通過源碼我們發(fā)現(xiàn)ThreadLocal主要提供了下面五個(gè)方法:
/** * 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. * * 返回此線程局部變量的當(dāng)前線程副本中的值。 * 如果該變量沒有當(dāng)前線程的值,則首先將其初始化為調(diào)用{@link #initialValue}方法返回的值。 * @return the current thread's value of this thread-local */ public T get() { } /** * 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. * * 將此線程局部變量的當(dāng)前線程副本設(shè)置為指定值。 * 大多數(shù)子類將不需要重寫此方法,而僅依靠{@link #initialValue}方法來設(shè)置線程局部變量的值。 * * @param value the value to be stored in the current thread's copy of * this thread-local. * 要存儲(chǔ)在此本地線程的當(dāng)前線程副本中的值。 */ public void set(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. * 刪除此線程局部變量的當(dāng)前線程值。 * 如果此線程局部變量隨后被當(dāng)前線程{@linkplain #get read}調(diào)用, * 則其值將通過調(diào)用其{@link #initialValue}方法來重新初始化, * 除非當(dāng)前值是在此期間被設(shè)置{@linkplain #set set}。 * 這可能會(huì)導(dǎo)致在當(dāng)前線程中多次調(diào)用{@code initialValue}方法。 * @since 1.5 */ public void remove() { } /** * Returns the current thread's "initial value" for this * thread-local variable. This method will be invoked the first * time a thread accesses the variable with the {@link #get} * method, unless the thread previously invoked the {@link #set} * method, in which case the {@code initialValue} method will not * be invoked for the thread. Normally, this method is invoked at * most once per thread, but it may be invoked again in case of * subsequent invocations of {@link #remove} followed by {@link #get}. * 返回此線程局部變量的當(dāng)前線程的“初始值”。 * 除非線程先前調(diào)用了{(lán)@link #set}方法, * 否則線程第一次使用{@link #get}方法訪問該變量時(shí)將調(diào)用此方法, * 在這種情況下,{@ code initialValue}方法將不會(huì)為線程被調(diào)用。 * 通常,每個(gè)線程最多調(diào)用一次此方法, * 但是在隨后調(diào)用{@link #remove}之后再調(diào)用{@link #get}的情況下,可以再次調(diào)用此方法。 * * <p>This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * 此實(shí)現(xiàn)僅返回{@code null};如果程序員希望線程局部變量的初始值不是{@code null}, * 則必須將{@code ThreadLocal}子類化,并重寫此方法。通常,將使用匿名內(nèi)部類。 * * @return the initial value for this thread-local */ protected T initialValue(){ }
3.1 get方法
public T get() { //獲取當(dāng)前線程 Thread t = Thread.currentThread(); //通過當(dāng)前線程獲取ThreadLocalMap //Thread類中包含一個(gè)ThreadLocalMap的成員變量 ThreadLocalMap map = getMap(t); //如果不為空,則通過ThreadLocalMap中獲取對應(yīng)value值 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //如果為空,需要初始化值 return setInitialValue(); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else //如果為空,則創(chuàng)建 createMap(t, value); return value; }
首先是取得當(dāng)前線程,然后通過getMap(t)方法獲取到一個(gè)map,map的類型為ThreadLocalMap。然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進(jìn)去的是 this,而不是當(dāng)前線程t。 如果獲取成功,則返回value值。如果map為空,則調(diào)用setInitialValue方法返回value。
在setInitialValue方法中,首先執(zhí)行了initialValue方法(我們上面提到的最后一個(gè)方法),接著通過當(dāng)前線程獲取ThreadLocalMap,如果不存在則創(chuàng)建。創(chuàng)建的代碼很簡單,只是通過ThreadLocal對象和設(shè)置的Value值創(chuàng)建ThreadLocalMap對象。
3.2 set方法
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
這個(gè)方法和setInitialValue方法的業(yè)務(wù)邏輯基本相同,只不過setInitialValue調(diào)用了initialValue()的鉤子方法。這里代碼簡單,我們就不做過多解釋。
3.3 remove方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
這個(gè)方法是從jdk1.5才出現(xiàn)的。處理邏輯也很很簡單。通過當(dāng)前線程獲取到ThreadLocalMap對象,然后移除此ThreadLocal。
3.4 initialValue方法
protected T initialValue() { return null; }
是不是感覺簡單了,什么也沒有處理,直接返回一個(gè)null,那么何必如此設(shè)計(jì)呢?當(dāng)我們發(fā)現(xiàn)他的修飾符就會(huì)發(fā)現(xiàn),他應(yīng)該是一個(gè)鉤子方法,主要用于提供子類實(shí)現(xiàn)的。追溯到源碼中我們發(fā)現(xiàn),Supplier的影子,這就是和jdk8的lamda表達(dá)式關(guān)聯(lián)上了。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { return supplier.get(); } }
4. 總結(jié)
在每個(gè)線程Thread內(nèi)部有一個(gè)ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個(gè)threadLocals就是用來存儲(chǔ)實(shí)際的變量副本的,鍵值為當(dāng)前ThreadLocal變量,value為變量副本(即T類型的變量)。之所以這里是一個(gè)map,是因?yàn)橥ㄟ^線程會(huì)存在多個(gè)類中定義ThreadLocal的成員變量。初始時(shí),在Thread里面,threadLocals為空,當(dāng)通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會(huì)對Thread類中的threadLocals進(jìn)行初始化,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals; 然后在當(dāng)前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。
5. ThreadLocalMap引發(fā)的內(nèi)存泄漏
ThreadLocal屬于一個(gè)工具類,他為用戶提供get、set、remove接口操作實(shí)際存放本地變量的threadLocals(調(diào)用線程的成員變量),也知道threadLocals是一個(gè)ThreadLocalMap類型的變量。下面我們來看看ThreadLocalMap這個(gè)類的一個(gè)entry:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object val Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } public WeakReference(T referent) { super(referent); //referent:ThreadLocal的引用 } //Reference構(gòu)造方法 Reference(T referent) { this(referent, null);//referent:ThreadLocal的引用 } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
在上面的代碼中,我們可以看出,當(dāng)前ThreadLocal的引用k被傳遞給WeakReference的構(gòu)造函數(shù),所以ThreadLocalMap中的key為ThreadLocal的弱引用。當(dāng)一個(gè)線程調(diào)用ThreadLocal的set方法設(shè)置變量的時(shí)候,當(dāng)前線程的ThreadLocalMap就會(huì)存放一個(gè)記錄,這個(gè)記錄的key值為ThreadLocal的弱引用,value就是通過set設(shè)置的值。如果當(dāng)前線程一直存在且沒有調(diào)用該ThreadLocal的remove方法,如果這個(gè)時(shí)候別的地方還有對ThreadLocal的引用,那么當(dāng)前線程中的ThreadLocalMap中會(huì)存在對ThreadLocal變量的引用和value對象的引用,是不會(huì)釋放的,就會(huì)造成內(nèi)存泄漏。
考慮這個(gè)ThreadLocal變量沒有其他強(qiáng)依賴,如果當(dāng)前線程還存在,由于線程的ThreadLocalMap里面的key是弱引用,所以當(dāng)前線程的ThreadLocalMap里面的ThreadLocal變量的弱引用在gc的時(shí)候就被回收,但是對應(yīng)的value還是存在的這就可能造成內(nèi)存泄漏(因?yàn)檫@個(gè)時(shí)候ThreadLocalMap會(huì)存在key為null但是value不為null的entry項(xiàng))。
總結(jié):THreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,在沒有其他地方對ThreadLoca依賴,ThreadLocalMap中的ThreadLocal對象就會(huì)被回收掉,但是對應(yīng)的不會(huì)被回收,這個(gè)時(shí)候Map中就可能存在key為null但是value不為null的項(xiàng),這需要實(shí)際的時(shí)候使用完畢及時(shí)調(diào)用remove方法避免內(nèi)存泄漏。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java線程池FutureTask實(shí)現(xiàn)原理詳解
這篇文章主要介紹了Java線程池FutureTask實(shí)現(xiàn)原理詳解,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02javafx tableview鼠標(biāo)觸發(fā)更新屬性詳解
這篇文章主要為大家詳細(xì)介紹了javafx tableview鼠標(biāo)觸發(fā)更新屬性的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08使用log4j2打印mybatis的sql執(zhí)行日志方式
這篇文章主要介紹了使用log4j2打印mybatis的sql執(zhí)行日志方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09SpringBoot + minio實(shí)現(xiàn)分片上傳、秒傳、續(xù)傳功能
MinIO是一個(gè)基于Go實(shí)現(xiàn)的高性能、兼容S3協(xié)議的對象存儲(chǔ),使用MinIO構(gòu)建用于機(jī)器學(xué)習(xí),分析和應(yīng)用程序數(shù)據(jù)工作負(fù)載的高性能基礎(chǔ)架構(gòu),這篇文章主要介紹了SpringBoot + minio實(shí)現(xiàn)分片上傳、秒傳、續(xù)傳,需要的朋友可以參考下2023-06-06SpringBoot分頁查詢功能的實(shí)現(xiàn)方法
在實(shí)際的項(xiàng)目開發(fā)過程中,分頁顯示是很常見的頁面布局,所以學(xué)習(xí)如何實(shí)現(xiàn)分頁也是必要的,下面這篇文章主要給大家介紹了關(guān)于SpringBoot分頁查詢功能的實(shí)現(xiàn)方法,需要的朋友可以參考下2022-06-06