1. 程式人生 > >ThreadLocal原始碼分析(JDK8)

ThreadLocal原始碼分析(JDK8)

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 intHASH_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表示式賦值而已。使用事例如下:
@Test
public 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中。
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:若衝突則將新資料按連結串列或紅黑樹邏輯插入。 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! 
將來的你一定會感激現在拼命的自己!