ThreadLocal特性及使用情境: 1、方便同一個線程使用某一對象,避免不必要的參數傳遞; 2、線程間資料隔離(每個線程在自己線程裡使用自己的局部變數,各線程間的ThreadLocal對象互不影響); 3、擷取資料庫連接、Session、關聯ID(比如日誌的uniqueID,方便串起多個日誌);
ThreadLocal應注意: 1、ThreadLocal並未解決多線程訪問共用對象的問題; 2、ThreadLocal並不是每個線程拷貝一個對象,而是直接new(建立)一個; 3、如果ThreadLocal.set()的對象是多線程共用的,那麼還是涉及並發問題。
1、ThreadLocal<T>初始化 private final int threadLocalHashCode =
nextHashCode(); private static final int HASH_INCREMENT= 0x61c88647; /** * The next hash code to be given out. Updated atomically. Starts at zero. */ // 源碼說nextHashCode初始值為0,但實際調試時顯示初始值為1253254570,費解。 // 而且當初始化完畢後,nextHashCode的值又變為0,說明其初始值確實是0的。
private static AtomicInteger nextHashCode = new AtomicInteger();private static intnextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);} ThreadLocal類變數有3個,其中2個是靜態變數(包括一個常量),實際作為作為ThreadLocal執行個體的變數只有threadLocalHashCode這1個,而且已經初始化就不可變了。 建立ThreadLocal執行個體時有哪些操作呢: ThreadLocal初始化時會調用nextHashCode()方法初始化threadLocalHashCode,且threadLocalHashCode 初始化後不可變。 threadLocalHashCode可用來標記不同的ThreadLocal執行個體。
2、內部類 2.1 ThreadLocalMap ThreadLocalMap是 定製的hashMap,僅用於維護當前線程的本地變數值。僅ThreadLocal類對其有操作許可權,是Thread的私人屬性。為避免佔用空間較大或生命週期較長的資料常駐於記憶體引發一系列問題,hash table的key是弱引用WeakReferences。當空間不足時,會清理未被引用的entry。 ThreadLocalMap中的重點:
static class Entryextends WeakReference<ThreadLocal<?>>{ /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v){ super(k); value= v; }}Note: ThreadLocalMap的key是ThreadLocal,value是Object(即我們所謂的“執行緒區域資料”)。
2.2 SuppliedThreadLocal<T> extends ThreadLocal<T> SuppliedThreadLocal是JDK8新增的內部類,只是擴充了ThreadLocal的初始化值的方法而已,允許使用JDK8新增的Lambda運算式賦值。需要注意的是,函數式介面Supplier不允許為null。 源碼如下:
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(); }}3、主要方法 3.1、T get() 返回當前線程的value。
public T get(){ Thread t= Thread.currentThread(); // 擷取當前線程 ThreadLocalMap map= getMap(t); // 擷取當前線程對應的Map if(map!=null){ ThreadLocalMap.Entry e = map.getEntry(this); // 詳見3.1.1 if(e!=null){ // map不為空白且當前線程有value,返回value @SuppressWarnings("unchecked") T result=(T)e.value; return result; } } return setInitialValue(); // 初始化再傳回值}----- getMap的源碼:
ThreadLocalMap getMap(Thread t){ returnt.threadLocals;} getMap(t)返回當前線程的成員變數ThreadLocalMap(Thread的成員變數有ThreadLocalMap,這一點可以查看Thread的源碼,如下)很明確的說明了 ThreadLocal屬於線程,ThreadLocalMap由ThreadLocal持有,說到底,ThreadLocalMap 也是線程所持有。每個線程Thread都有自己的ThreadLocalMap。 /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
-------- setInitialValue源碼:
private T setInitialValue(){ T value= initialValue(); //調用重寫的initialValue,返回新值 Thread t= Thread.currentThread(); ThreadLocalMap map= getMap(t); if(map!=null) // 當前線程的ThreadLocalMap不為空白,則直接賦值 map.set(this, value); else// 為當前線程創造一個ThreadLocalMap(this, firstValue)並賦初值,this為當前線程 createMap(t, value); return value;}protected T initialValue() { return T; // 自訂傳回值}; createMap源碼:
void createMap(Thread t, T firstValue){ t.threadLocals=new ThreadLocalMap(this, firstValue);}ThreadLocal之get流程: 1、擷取當前線程t; 2、返回當前線程t的成員變數ThreadLocalMap(以下簡寫map); 3、map不為null,則擷取以當前線程為key的ThreadLocalMap的Entry(以下簡寫e),如果e不為null,則直接返回該Entry的value; 4、如果map為null或者e為null,返回setInitialValue()的值。setInitialValue()調用重寫的 initialValue()返回新值(如果沒有重寫initialValue將返回預設值null ),並將新值存入當前線程的ThreadLocalMap(如果當前線程沒有ThreadLocalMap,會先建立一個)。
3.2、void set(T value) 為【當前線程】的【當前ThreadLocal】賦值(初始值or新值)。和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);} 3.3、void remove() 擷取當前線程的ThreadLocalMap,map不為空白,則移除當前ThreadLocal作為key的索引值對。
public void remove(){ ThreadLocalMap m= getMap(Thread.currentThread()); if(m!=null) m.remove(this); }Note: remove()移除當前線程的當前ThreadLocal資料(只是清空該key-value索引值對),而且是 立即移除,移除後,再調用get方法將重新調用initialValue方法初始化(除非在此期間調用了set方法賦值)。
3.4、static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) JDK8新增,支援Lambda運算式,和 ThreadLocal重寫的initialValue()效果一樣。
public static<S> ThreadLocal<S> withInitial(Supplier<?extends S> supplier){ return new SuppliedThreadLocal<>(supplier);} 可以看出,withInitial()方法的入參是函數式介面Supplier,傳回值是JDK8新增的內部類SuppliedThreadLocal,正如2.2所說,區別僅在於支援Lambda運算式賦值而已。使用案例如下:
@Testpublic void jdk8Test(){ Supplier<String> supplier =new Supplier<String>(){ @Override public String get(){ return"supplier_new"; } }; threadLocal= ThreadLocal.withInitial(supplier); System.out.println(threadLocal.get());// supplier_new threadLocal= ThreadLocal.withInitial(()->"sup_new_2"); System.out.println(threadLocal.get());// sup_new_2 ThreadLocal<DateFormat> localDate = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd")); System.out.println(localDate.get().format(new Date()));// 2017-01-22 ThreadLocal<String> local =new ThreadLocal<>().withInitial(supplier); System.out.println(local.get());// supplier_new}Note: ·withInitial(supplier)是有傳回值ThreadLocal的,So執行個體化時需將其賦值給ThreadLocal執行個體。
4、圖解 ThreadLocal 每個線程可能有多個ThreadLocal,同一線程的各個ThreadLocal存放於同一個ThreadLocalMap中。
圖解ThreadLocal(JDK8).vsdx原圖下載地址: https://github.com/zxiaofan/JDK-Study/tree/master/src/java1/lang/threadLocal
5、 ThreadLocal-ThreadLocalMap源碼分析
5.1、Entry getEntry(ThreadLocal<?> key) 首先來看get方法,你會發現ThreadLocalMap的get方法和傳統Map不同,其返回的不是key-value的value,而是整個entry,當時entry的key是ThreadLocal,value是存放的值,這點是一致的。
a、getEntry源碼分析:
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);} getEnrty方法只會處理key被直接命中的entry,沒有直接命中的(key衝突的)資料將調用getEntryAfterMiss()方法返回對應enrty,按照源碼解釋,這樣做是為了儘可能提升直接命中的效能。
ThreadLocalMap之getEntry的流程: 1、計算Entry數組的index((length - 1) & key.hash)。 索引計算和HashMap的異同: ①相似之處:計算方式相同,均為(length - 1) & key.hash;length均為底層結構的大小(是大小,不是實際size)。 ②不同之處:HashMap(JDK8)底層資料結構是 位桶+鏈表/紅/黑樹狀結構,而ThreadLocalMap底層資料結構是 Entry數組;HashMap的key.hash的計算方式是 native、異或、無符號位移,(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),ThreadLocalMap的key.hash從ThreadLocal執行個體化時便由nextHashCode()確定。 2、擷取對應index的節點Entry; 3、如果返回節點entry 有值且其key未衝突(只有1個即entry返回的key等於傳入的key),則直接返回該entry; 4、返回entry為空白或鍵衝突,則調用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法返回entry。
b、getEntryAfterMiss源碼分析: getEntryAfterMiss處理那些getEntry時沒有被命中的key(value為空白的直接返回null,so更確切的說是命中且有衝突的key)。入參是當前ThreadLocal,key在數組的索引index,以及index對應的索引值對。
private Entry getEntryAfterMiss(ThreadLocal<?> key,int i, Entry e){ Entry[] tab = table; int len= tab.length; 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之getEntryAfterMiss的流程: 僅分析Entry不為空白的情況, 1、擷取entry的key; 2、如果key一致(記憶體位址=判斷),則返回該entry; 3、如果key為null,則調用expungeStaleEntry方法 擦除該entry; 4、其他情況則通過nextIndex方法擷取下一個索引位置index; 5、擷取新index處的entry,再死迴圈2/3/4,直到定位到該key返回entry或者返回null。
private static int nextIndex(int i,int len){ return((i+1< len)? i+1:0); // 把索引加1即可}c、expungeStaleEntry源碼分析: 只要key為null均會被擦除,使得對應value沒有被引用,方便回收。
private int expungeStaleEntry(int staleSlot){ Entry[] tab = table; int len= tab.length; // expunge entry at staleSlot tab[staleSlot].value=null; // 擦除當前index處value tab[staleSlot]=null; // 擦除當前index處key size--; // Rehash until we encounter null Entry e; int i; for(i= nextIndex(staleSlot, len); // 計算下一個index (e= tab[i])!=null; // 新index處entry不為空白 i= nextIndex(i, len)){ // 計算下一個index ThreadLocal<?> k = e.get(); // 擷取新key(ThreadLocal) if(k==null){ // key為null,再次置空 e.value=null; tab[i]=null; size--; }else{ int h= k.threadLocalHashCode&(len-1); // 計算新index if(h!= i){ // index若未變化,說明沒有多餘的entry了 tab[i]=null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale.// 一直掃到最後一個非空位置,將其值置為碰撞處第一個entry。 while(tab[h]!=null) h= nextIndex(h, len); tab[h]= e; } } } return i;}
5.2、set(ThreadLocal<?> key, Object value)
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; int i= key.threadLocalHashCode&(len-1); for(Entry e= tab[i]; e !=null;e= tab[i= nextIndex(i, len)]){// 當前index處已有entry ThreadLocal<?> k = e.get(); if(k== key){ // key(ThreadLocal)相同,更新value e.value= value; return; } if(k==null){ // 出現到期資料// 遍曆清洗到期資料並在index處插入新資料,其他資料後移 replaceStaleEntry(key, value, i); return; } } tab[i]=new Entry(key, value); int sz=++size;// 沒有到期資料被清理且實際size超過擴容閾值 if(!cleanSomeSlots(i, sz)&& sz>= threshold) rehash();}
rehash(): size:table的實際entry數量;擴容閾值threshold:table.lenrth(預設16)大小的2/3; 首先調用expungeStaleEntries刪除所有到期資料,如果清理資料後size>=threshold的3/4,則2倍擴容。 ps:閾yù值又叫臨界值,是指一個效應能夠產生的最低值或最高值。閥fá 控制、開關、把持。
ThreadLocalMap和HashMap在hash衝突時的解決方案對比: HashMap:若衝突則將新資料按鏈表或紅/黑樹狀結構邏輯插入。 詳見《 HashMap源碼分析(jdk1.8)》
put(K key, V value)的邏輯: 1、判斷索引值對數組tab[]是否為空白或為null,是則resize(); 2、根據索引值key的hashCode()計算hash值得到當前Node的索引i(
bucketIndex ),如果tab[i]==null【沒碰撞】,直接建立節點添加,否則【碰撞】轉入3 3、判斷當前數組中處理hash衝突的方式為紅/黑樹狀結構還是鏈表(check第一個節點類型即可),分別處理。【①是紅/黑樹狀結構則按紅/黑樹狀結構邏輯插入;②是鏈表,則遍曆鏈表,看是否有key相同的節點;③有則更新value值,沒有則建立節點,此時若 鏈表數量大於閥值8【9個】,則調用treeifyBin方法(此方法先判斷table是否為null或tab.length小於64,是則執行resize操作,否則才將鏈表改為紅/黑樹狀結構)。】 4、如果size+1> threshold則resize。
ThreadLocalMap: 1、若指定位置index已有資料entry,逐個遍曆entry: 1.1、若index處key相同,則更新value; 1.2、若index處key為null,則調用replaceStaleEntry清理到期資料並插入新資料(從index處挨個遍曆,直到找到相同key更新value結束,或者一直未找到,則在index處放入new Entry)。replaceStaleEntry遍曆時會將entry逐個後移,也就是說set進去的最新entry一定會放在index處,方便get時直接命中。 2、index處無資料,則放入新entry;隨後清理到期資料並判斷是否2倍擴容(size>=threshold的3/4)。
參考資料: http://www.cnblogs.com/dolphin0520/p/3920407.html 有任何問題,歡迎指正探討。
歡迎個人轉載,但須在文章頁面明顯位置給出原文串連;未經作者同意必須保留此段聲明、不得隨意修改原文、不得用於商業用途,否則保留追究法律責任的權利。【 CSDN 】:csdn.zxiaofan.com【GitHub】:github.zxiaofan.com如有任何問題,歡迎留言。祝君好運。Life is all about choices。 將來的你一定會感激現在拚命的自己。