ThreadLocal-單例模式下高并發(fā)線程安全
在多例的情況下,每個對象在堆中聲明內(nèi)存空間,多線程對應(yīng)的Java棧中的句柄或指針指向堆中不同的對象,對象各自變量的變更只會印象到對應(yīng)的棧,也就是對應(yīng)的線程中,不會影響到其它線程。所以多例的情況下不需要考慮線程安全的問題,因為一定是安全的。
而在單例的情況下卻完全不一樣了,在堆中只有一個對象,多線程對應(yīng)的Java棧中的句柄或指針指向同一個對象,方法的參數(shù)變量和方法內(nèi)變量是線程安全的,因為每執(zhí)行一個方法,都會在獨立的空間創(chuàng)建局部變量,它不是共享的資源。但是成員變量只有一份,所有指向堆中該對象的句柄或指針都可以隨時修改和讀取它,所以是非線程安全的。
為了解決線程安全的問題,我們有3個思路:
- 第一每個線程獨享自己的操作對象,也就是多例,多例勢必會帶來堆內(nèi)存占用、頻繁GC、對象初始化性能開銷等待等一些列問題。
- 第二單例模式枷鎖,典型的案例是HashTable和HashMap,對讀取和變更的操作用synchronized限制起來,保證同一時間只有一個線程可以操作該對象。雖然解決了內(nèi)存、回收、構(gòu)造、初始化等問題,但是勢必會因為鎖競爭帶來高并發(fā)下性能的下降。
- 第三個思路就是今天重點推出的ThreadLocal。單例模式下通過某種機(jī)制維護(hù)成員變量不同線程的版本。
- 假設(shè)三個人想從鏡子中看自己,第一個方案就是每人發(fā)一個鏡子互不干擾,第二個方案就是只有一個鏡子,一個人站在鏡子前其他人要排隊等候,第三個方案就是我這里發(fā)明了一種“魔鏡”,所有人站在鏡子前可以并且只能看到自己!!!
主程序:
public static void main(String[] args) { //Mirror是個單例的,只構(gòu)建了一個對象 Mirror mirror = new Mirror("魔鏡"); //三個線程都在用這面鏡子 MirrorThread thread1 = new MirrorThread(mirror,"張三"); MirrorThread thread2 = new MirrorThread(mirror,"李四"); MirrorThread thread3 = new MirrorThread(mirror,"王二"); thread1.start(); thread2.start(); thread3.start(); }
很好理解,創(chuàng)建了一面鏡子,3個人一起照鏡子。
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)?quot;); sb.append(lookMirror()); System.out.println(sb); } catch (InterruptedException e) { e.printStackTrace(); } i++; } } }
也很好理解,就是不斷的更新自己的外貌同時從鏡子里讀取自己的外貌。
重點是Mirror:
public class Mirror { private String mirrorName; //每個人要看到自己的樣子,所以這里要用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; } }
對每個人長的樣子用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,一面鏡子所有人一起照,而且每個人都只能看的到自己的變化,這就達(dá)成了單例線程安全的目的。
我們來細(xì)看下它是怎么實現(xiàn)的。
先來看Thread:
Thread中維護(hù)了一個ThreadLocal.ThreadLocalMapthreadLocals = null; ThreadLocalMap這個Map的key是ThreadLocal,value是維護(hù)的成員變量。現(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:它里面核心方法兩個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存儲到當(dāng)前線程對象的threadLocals中,也就是TreadLocalMap中。
OK,這樣整個關(guān)系鏈已經(jīng)建立,真正要去訪問的成員變量在一個map中,key是線程號,值是屬于該線程的快照。
ThreadLocal里還有map的創(chuàng)建createMap(t, value)、取值時對象的初始值setInitialValue()、線程結(jié)束時對象的釋放remove()等細(xì)節(jié),有興趣的可以繼續(xù)跟進(jìn)了解下。
ThreadLocal應(yīng)用其實很多,例如Spring容器中實例默認(rèn)是單例的,transactionManager也一樣,那么事務(wù)在處理時單例的manager是如何控制每個線程的事務(wù)要如何處理呢,這里面就應(yīng)用了大量的ThreadLocal。
多線程中的ThreadLocal
1.ThreadLocal概述
多線程的并發(fā)問題主要存在于多個線程對于同一個變量進(jìn)行修改產(chǎn)生的數(shù)據(jù)不一致的問題,同一個變量指的值同一個對象的成員變量或者是同一個類的靜態(tài)變量。之前我們常聽過盡量不要使用靜態(tài)變量,會引起并發(fā)問題,那么隨著Spring框架的深入人心,單例中的成員變量也出現(xiàn)了多線程并發(fā)問題。Struts2接受參數(shù)采用成員變量自動封裝,為此在Spring的配置采用多例模式,而SpringMVC將Spring的容器化發(fā)揮到極致,將接受的參數(shù)放到了注解和方法的參數(shù)中,從而避免了單例出現(xiàn)的線程問題。今天,我們討論的是JDK從1.2就出現(xiàn)的一個并發(fā)工具類ThreadLocal,他除了加鎖這種同步方式之外的另一種保證一種規(guī)避多線程訪問出現(xiàn)線程不安全的方法,當(dāng)我們在創(chuàng)建一個變量后,如果每個線程對其進(jìn)行訪問的時候訪問的都是線程自己的變量這樣就不會存在線程不安全問題。我們先看一下官方是怎么解釋這個變量的?
大致意思是:此類提供了局部變量表。這些變量與普通變量不同不同之處是,每一個通過get或者set方法訪問一個線程都是他自己的,將變量的副本獨立初始化。ThreadLocal實例通常作用于希望將狀態(tài)與線程關(guān)聯(lián)的類中的私有靜態(tài)字段(例如,用戶ID或交易ID)。
只要線程是活動的并且可以訪問{@code ThreadLocal}實例, 每個線程都會對其線程局部變量的副本保留隱式引用。 線程消失后,其線程本地實例的所有副本都將進(jìn)行垃圾回收(除非存在對這些副本的其他引用)。也就是說,如果創(chuàng)建一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內(nèi)存中的變量,從而規(guī)避了線程安全問題。而每個線程的副本全部放到ThreadLocalMap中。
2. ThreadLocal簡單實用
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中,不會出現(xiàn)多線程并發(fā)問題,而成員變量variable卻發(fā)生了多線程并發(fā)問題。
3.ThreadLocal的內(nèi)部原理
通過源碼我們發(fā)現(xiàn)ThreadLocal主要提供了下面五個方法:
/** * 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. * 要存儲在此本地線程的當(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}。 * 這可能會導(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}方法訪問該變量時將調(diào)用此方法, * 在這種情況下,{@ code initialValue}方法將不會為線程被調(diào)用。 * 通常,每個線程最多調(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. * 此實現(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類中包含一個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)方法獲取到一個map,map的類型為ThreadLocalMap。然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進(jìn)去的是 this,而不是當(dāng)前線程t。 如果獲取成功,則返回value值。如果map為空,則調(diào)用setInitialValue方法返回value。
在setInitialValue方法中,首先執(zhí)行了initialValue方法(我們上面提到的最后一個方法),接著通過當(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); }
這個方法和setInitialValue方法的業(yè)務(wù)邏輯基本相同,只不過setInitialValue調(diào)用了initialValue()的鉤子方法。這里代碼簡單,我們就不做過多解釋。
3.3 remove方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
這個方法是從jdk1.5才出現(xiàn)的。處理邏輯也很很簡單。通過當(dāng)前線程獲取到ThreadLocalMap對象,然后移除此ThreadLocal。
3.4 initialValue方法
protected T initialValue() { return null; }
是不是感覺簡單了,什么也沒有處理,直接返回一個null,那么何必如此設(shè)計呢?當(dāng)我們發(fā)現(xiàn)他的修飾符就會發(fā)現(xiàn),他應(yīng)該是一個鉤子方法,主要用于提供子類實現(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é)
在每個線程Thread內(nèi)部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當(dāng)前ThreadLocal變量,value為變量副本(即T類型的變量)。之所以這里是一個map,是因為通過線程會存在多個類中定義ThreadLocal的成員變量。初始時,在Thread里面,threadLocals為空,當(dāng)通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會對Thread類中的threadLocals進(jìn)行初始化,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals; 然后在當(dāng)前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。
5. ThreadLocalMap引發(fā)的內(nèi)存泄漏
ThreadLocal屬于一個工具類,他為用戶提供get、set、remove接口操作實際存放本地變量的threadLocals(調(diào)用線程的成員變量),也知道threadLocals是一個ThreadLocalMap類型的變量。下面我們來看看ThreadLocalMap這個類的一個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)一個線程調(diào)用ThreadLocal的set方法設(shè)置變量的時候,當(dāng)前線程的ThreadLocalMap就會存放一個記錄,這個記錄的key值為ThreadLocal的弱引用,value就是通過set設(shè)置的值。如果當(dāng)前線程一直存在且沒有調(diào)用該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那么當(dāng)前線程中的ThreadLocalMap中會存在對ThreadLocal變量的引用和value對象的引用,是不會釋放的,就會造成內(nèi)存泄漏。
考慮這個ThreadLocal變量沒有其他強(qiáng)依賴,如果當(dāng)前線程還存在,由于線程的ThreadLocalMap里面的key是弱引用,所以當(dāng)前線程的ThreadLocalMap里面的ThreadLocal變量的弱引用在gc的時候就被回收,但是對應(yīng)的value還是存在的這就可能造成內(nèi)存泄漏(因為這個時候ThreadLocalMap會存在key為null但是value不為null的entry項)。
總結(jié):THreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,在沒有其他地方對ThreadLoca依賴,ThreadLocalMap中的ThreadLocal對象就會被回收掉,但是對應(yīng)的不會被回收,這個時候Map中就可能存在key為null但是value不為null的項,這需要實際的時候使用完畢及時調(diào)用remove方法避免內(nèi)存泄漏。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持服務(wù)器之家。
原文鏈接:https://yejingtao.blog.csdn.net/article/details/78806902