并发基础(十) 线程局部副本ThreadLocal之正解
什么是ThreadLocal
ThreadLocal是線程局部變量,所謂的線程局部變量,就是僅僅只能被本線程訪問,不能在線程之間進行共享訪問的變量。在各個Java web的各種框架中ThreadLocal幾乎已經被用爛了,spring中有使用,mybatis中也有使用,hibernate中也有使用,甚至我們寫個分頁也用ThreadLocal來傳遞參數…這也從側面說明了ThreadLocal十分的給力。
下面看看作者Doug Lea是怎么說的,下面是jdk7.X中的注釋:
也就是說這個類給線程提供了一個本地變量,這個變量是該線程自己擁有的。在該線程存活和ThreadLocal實例能訪問的時候,保存了對這個變量副本的引用.當線程消失的時候,所有的本地實例都會被GC。并且建議我們ThreadLocal最好是 private static 修飾的成員
ThreadLocal和Synchonized區別
都用于解決多線程并發訪問。
Synchronized用于線程間的數據共享(使變量或代碼塊在某一時該只能被一個線程訪問),是一種以延長訪問時間來換取線程安全性的策略;
而ThreadLocal則用于線程間的數據隔離(為每一個線程都提供了變量的副本),是一種以空間來換取線程安全性的策略。
ThreadLocal的簡單方法
先來看一下ThreadLocal的API:
1、構造方法摘要
ThreadLocal(): 創建一個線程本地變量。
2、方法摘要
void set(T value): 將此線程局部變量的當前線程副本中的值設置為指定值。
T get(): 返回此線程局部變量的當前線程副本中的值。
void remove():移除此線程局部變量當前線程的值。
protected T initialValue():返回此線程局部變量的當前線程的“初始值”。是protected方法,是為了讓子類繼承而設計的。
簡單代碼應用
ThreadLocalTest類有兩個方法,一個是start方法,一個是end方法,start記錄開始時間,end方法記錄結束時間,這個方法可以簡單的用在統計耗時的功能上,在方法的入口前執行start,在方法被調用之后調用end方法,好處是兩個方法的調用不用再一個方法或者類中,比如在aop(面向切面編程)中,在方法調用前的切入點執行start方法,在方法調用之后調用end方法,這樣依舊可以得到方法執行的耗時。
ThreadLocal 類很簡單,下面接著拋出兩個誤區,以這兩個誤區為起點,進行分析,逐步揭開ThreadLocal 的真面目。
一、兩大誤區
1、誤區一 Threadlocal 的出現是為了解決多線程共享對象的問題。
網上不少的文章對ThreadLocal有著很糟糕的錯誤認識,認為ThreadLocal可以為每一個共享對象保持一個副本,這樣就可以解決多線程并發競爭資源的問題。本人在入門并發時,也是這么認為的,但隨著工作實戰的經驗增加,根本就不是那么一回事。
??我們來分析一下。假設ThreadLocal是能夠解決多線程共享對象的問題,于是我們為每一個線程都維護一個該對象的獨立副本(先不考慮內存的問題)。如果都是讀線程,那么問題不大。但如果有寫線程呢?寫線程修改了副本,但是其他讀線程讀取到還是舊的值,這樣線程之間無法通信,共享對象就失去意義了(共享對象是線程通信的一種方式,在一個線程修改了,另一個線程也應該看的見)。如果僅僅都是讀線程,要維護這么多副本,消耗大量內存,而且在多線程的環境下,只能讀取的話,可以不加鎖,那么競爭就不存在,不需要額外維護多個副本。經過上面的分析,ThreadLocal 是不可能解決多線程共享對象的問題
那么 ThreadLocal 的真正作用是什么呢?
看一下JDK的源碼注釋:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
對應的中文應該是這樣的:
該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對應物,因為訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
好像還是不太懂,那么再具體一點就是: 如果當類中的某個變量希望根據不同的線程提供不同的值,而且任意一個線程修改這個變量不會影響到其他線程,那么這個變量就應該是線程的局部變量。 一般的用法是用 private static 修飾變量,這是因為ThreadLocalMap中的key值是一個弱引用,是以ThreadLocal為key,所以要用static來延長ThreadLocal的生存時間,后續講到。
誤區二 ThreadLocal 的底層實現是一個Map,key是當前線程,value是局部變量
ThreadLocal的底層維護著一個Map,key是 Thread.currentThread(當前線程),value 則是要保持的局部變量。這種設計思路是所有人最容易想到的,也是最容易被大家所誤解的方案,其實這也是早期JDK的設計方案(好像是JDK1.2)。(簡稱為 方案A)
但后期的JDK中,改善了ThreadLocal的設計,也是本文的重點,先簡單說一下設計:不同于方案A的只有一個Map的設計,此方案的每一個Thread 對象中各自維護著一個ThreadLocal.ThreadLocalMap 對象(可以看成是一個簡答的Map),此Map對象是線程私有的,key是ThreadLocal對象,value是線程的局部變量。而ThreadLocal中沒有維護著Map對象。(簡稱為 方案B)
方案A的設計有什么問題?為什么被拋棄?
方案A之所以被拋棄,因為以下幾點原因
- 線程需要競爭ThreadLocal中的Map。 一般情況下,是多個線程程共享著一個ThreadLocal 對象,按照方案A的設計,意味著多個線程共享著一個Map對象,所以訪問這個Map對象時,需要進行同步互斥訪問,訪問速度將下降。
- 線程的局部變量在線程死亡時難以回收或者難以及時回收。 ThreadLocal的Map存儲了多個線程的局部變量,當其中任意一個線程銷毀時,其局部變量也應該跟著銷毀,以釋放內存。但是按照方案A中的設計,可能要遍歷所有的Map,逐一判斷線程(key值)的狀態是否死亡,才能釋放內存。如果這樣做,不僅性能低,且無法及時釋放內存,甚至可能會造成Map過大,內存溢出。
方案B有什么優點?方案B又是怎么解決的?
針對方案A的遇到的問題,方案B(目前方案)中都能得到解決:
-
不需要競爭訪問Map。 在方案B中,是每個Thread對象都維護著一個ThreadLocalMap,所以Map是線程私有的,不需要競爭。而且私有的Map只存儲一個線程的局部變量,存儲的元素的數量更少,那么hash沖突就少。這兩點都大大地提高訪問速度。
-
所有局部變量隨線程一起被銷毀回收。 因為Map是維護在線程Thread中,當線程被銷毀回收時,Map自然一起被銷毀回收。
-
key值是弱引用,盡可能地釋放過時的鍵值對Entry,回收內存。key值是指向ThreadLlocal的對象,采用了弱引用的設計,一旦此TreadLlocal對象沒有了強引用指向,將會在下次的GC中被回收,那么key值就會為null,對應的Entry對象也最終會被釋放,從而減少內存溢出的情況。
二、ThreadLocal的源碼解析
上面僅僅簡單地介紹了ThreadLocal的誤區和設計思路,并沒有深入去了解,也許你還是不太懂,那么接下來的部分將會通過源碼,深入分析線程局部變量的機制。
1、ThreadLocalMap 與 Thread、ThreadLocal 的關系
1.1、ThreadLocalMap 類是 ThreadLocal的靜態內部類
static class ThreadLocalMap {//.....}1.2、 ThreadLocalMap 對象是Thread的一個成員變量
//每個線程都維護著一個 存儲局部變量的MapThreadLocal.ThreadLocalMap threadLocals = null;1.3、ThreadLocalMap的幾個屬性
Entry[] table table數組是用來存儲鍵值對的。鍵值對的key值為ThreadLocal對象、value是線程局部變量
static class ThreadLocalMap {/*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two.*/private Entry[] table;/*** The number of entries in the table.*/private int size = 0;/*** The next size value at which to resize.*/private int threshold; // Default to 0//........}2、ThreadLocal 分析
2.1、ThreadLocal的三個屬性
ThreadLocal的屬性就只有以下三個,用于計算、保存ThreadLocal對象中的 threadLocalHashCode 的值,而且每個ThreadLocal對象的 threadLocalHashCode 是不一樣的,以此來區別它們,從而在 ThreadLocalMap 中減少hash沖突。
//當前的ThreadLocal 對象的hash值 private final int threadLocalHashCode = nextHashCode();//靜態變量,用于計算下一個hash值 private static AtomicInteger nextHashCode = new AtomicInteger();//hash增量值,參與下一個hash值的計算 private static final int HASH_INCREMENT = 0x61c88647;/*** Returns the next hash code.*/private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}2.2、ThreadLocal.set() 方法
public void set(T value) {Thread t = Thread.currentThread();//獲取當前線程的ThreadLocalMap 對象ThreadLocalMap map = getMap(t);if (map != null)//判斷Map是否創建map.set(this, value);//this 指代當前 threadLocl對象elsecreateMap(t, value);//為當前線程創建ThreadLocalMap對象}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}ThreadLocal的set方法很簡單。用當前線程中的 ThreadLocalMap 對象去存儲局部變量,map.set(this, value) key值為this所指代對象,也即調用了此set方法的ThreadLocal對象。
2.3、ThreadLocal.get() 方法
public T get() {Thread t = Thread.currentThread();//獲取當前線程的ThreadLocalMap對象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();//初始的value值為null}get()方法就更簡單了,調用 ThreadLocalMap.getEntry()方法,以當前調用get方法的ThreadLocal對象為key值,獲取對應的value值。
3、弱引用 與 ThreadLocalMap 的內存回收
先來看一下 Entry的源代碼,Entry類是定義在 ThreadLocalMap中的靜態內部類。
//繼承了 WeakReference static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {//創建了一個ThreadLocal對象的弱引用super(k);value = v;}}ThreadLocalMap.Entry 繼承了弱引用類 WeakReference 類,而且弱引用類包裹了key值。這意味著key值是一個弱引用。一旦key值所指向的ThreadLocal沒有了強引用指向,那么便會被下一次的GC回收。然后key值便會為null,但是對應的Entry對象還在,并沒有釋放內存,那ThreadLocalMap是如何回收內存的呢?
** ThreadLocalMap 的內存回收:是在getEntry()、set()、remove()時遍歷Map,將key值為null的Entry判斷為過時的Entry,然后便釋放掉這個Entry **。下面是重點講解set()方法。
private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;int len = tab.length;//通過key值(ThreadLocal對象)的散列值threadLocalHashCode計算出 Entry的索引位置int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {//獲取元素的key值ThreadLocal<?> k = e.get();if (k == key) {//hash命中,直接設置value值e.value = value;return;}if (k == null) {//沒有命中,但找到了過時的Entry對象,即key值為null//替換掉此過時的EntryreplaceStaleEntry(key, value, i);return;}}//如果即沒有命中,而且表中也沒有發現過時的Entry對象,則在當前空的位置創建并插入一個新的Entry來吃存儲tab[i] = new Entry(key, value);//表的大小增加int sz = ++size;//threshold = len * 2 / 3;判斷是否需要重hashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();//重hash}set()在設置值時,先計算出初始索引值,然后循環遍歷table數組,判斷table數組中的每個Entry是否匹配目標key,如果匹配則直接修改value值,如果發現有Entry過時,則調用replaceStaleEntry方法來替換掉這個過時的Entry,插入新的Entry,看一下replaceStaleEntry的源碼:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// Back up to check for prior stale entry in current run.// We clean out whole runs at a time to avoid continual// incremental rehashing due to garbage collector freeing// up refs in bunches (i.e., whenever the collector runs).//slotToExpunge記錄過時Entry的索引值int slotToExpunge = staleSlot;//以當前的過時Entry的索引staleSlot為起點,往后遍歷,尋找過時的Entryfor (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)//判斷是否是過時的EntryslotToExpunge = i;// Find either the key or trailing null slot of run, whichever// occurs first。以staleSlot為起點,繼續往后遍歷for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// If we find key, then we need to swap it// with the stale entry to maintain hash table order.// The newly stale slot, or any other stale slot// encountered above it, can then be sent to expungeStaleEntry// to remove or rehash all of the other entries in run.if (k == key) {//發現hash命中e.value = value;//直接修改value值//命中的Entry與過時的Entry交換位置tab[i] = tab[staleSlot];tab[staleSlot] = e;// 判斷前面的往后遍歷循環是否發現新的過時的Entry對象,即slotToExpunge記錄了新的索引值if (slotToExpunge == staleSlot)slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清除這個新發現的過時Entryreturn;}// If we didn't find stale entry on backward scan, the// first stale entry seen while scanning for key is the// first still present in the run.//發現了過時的Entry對象,如果前一個往后遍歷的循環沒有發現過時的Entry對象,才記錄當前的索引值。//優先釋放靠前的過時Entry對象if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slot//hash依舊沒有命中,那么就將當前的過時Entry給替換成新的Entry對象tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge themif (slotToExpunge != staleSlot)//判斷是否發現新的過時Entry對象cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}replaceStaleEntry()有點復雜,但是可以看出主要是兩點:
??
1、以當前的staleSlot為起點分別往前往后尋找過時的Entry對象并釋放;
2、無論是否找到目標key所對應的Entry,都替換掉staleSlot位置的過時Entry,換上新的Entry。
從JDK1.2版本開始,把對象的引用分為四種級別,從而使程序能更加靈活的控制對象的生命周期。這四種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
1.強引用
以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
2.軟引用(SoftReference)
如果一個對象只具有軟引用,那就類似于可有可物的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
3.弱引用(WeakReference)
如果一個對象只具有弱引用,那就類似于可有可物的生活用品。弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
4.虛引用(PhantomReference)
"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在于:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是 否已經加入了虛引用,來了解
被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
特別注意,在實際程序設計中一般很少使用弱引用與虛引用,使用軟用的情況較多,這是因為軟引用可以加速JVM對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
參考資料:
http://www.cnblogs.com/jinggod/p/8486370.html
https://blog.csdn.net/u011276324/article/details/66968995
總結
以上是生活随笔為你收集整理的并发基础(十) 线程局部副本ThreadLocal之正解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 并发基础(九) java线程的终止与中断
- 下一篇: bat等大公司常考java多线程面试题