在遇到線程安全問題的時候,咱們通常都是使用同步來解決,好比內置鎖、顯示鎖等等。線程安全的主要原由是由於多個線程同時操做一個共享變量,若是咱們換種思路,在某些場景下,咱們爲這些線程提供共享變量的副本,讓他們在本身的私有域中去操做這些變量,線程之間互不影響,那是否是就不會產生線程安全問題了?ThreadLocal提供了這樣的一種實現。html
ThreadLocal內部封裝了ThreadLocalMap結構來爲線程提供存儲數據的私有域空間,而Thread類提供了成員變量threadLocals來ThreadLocalMap,這樣ThreadLocal、TreadLocalMap、Thread就緊密聯繫起來了。ThreadLocal對外提供了get、set、remove等方法來供咱們操做Thread的私有域空間ThreadLocalMap。這裏咱們先說個大概,後面分析源碼的時候再來一一解釋。java
接下來直接看ThreadLocal的源碼。數組
ThreadLocal的類結構安全
ThreadLocal的類是java.lang包下的一個普通類,沒有任何類的繼承與接口實現。數據結構
public class ThreadLocal<T> { ...... }
ThreadLocal的成員變量this
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647;
ThreadLocal的構造方法線程
public ThreadLocal() {}
ThreadLocal的內部類
ThreadLocalMap:code
咱們第一眼就看到ThreadLocalMap中又有一個內部類Entry,好,咱們一個一個看。htm
Entry:對象
Entry就是ThreadLocalMap中實際存放數據的單個節點,爲了便於理解,咱們能夠參照HashMap中的Node節點。Entry組成的數組就是ThreadLocalMap的底層封裝數據的數據結構。
Entry繼承於WeakReference(弱引用),對於弱引用,咱們先作個大概的瞭解。
若是一個對象僅被WeakReference指向,而沒有其餘任何強引用指向的話,在下一次GC的時候,弱引用指向的對象就會被回收。
//ThreadLocalMap的map中定義內部類Entry,Entry就是具體存儲數據的結構 //Entry繼承了弱引用 //Entry的key是啥?是ThreadLocal的弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { //存放的數據 Object value; //Entry的構造方法 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry中有兩個成員變量,一個是Ojbect類型的value,還有一個是繼承於WeakReference的類型爲ThreadLocal的reference。咱們能夠把reference看作是key。
接着繼續看ThreadLocalMap中的成員變量和構造方法。
static class ThreadLocalMap { //節點數組的初始化容量值 private static final int INITIAL_CAPACITY = 16; //Entry節點數組,存放數據的數組 private Entry[] table; //Entry數組中實際存儲數據的數目,初始爲0 private int size = 0; //Entry數組擴容的閾值 private int threshold; //ThreadLocalMap的構造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化Entry數組,容量爲默認的初始值16 table = new Entry[INITIAL_CAPACITY]; //threadLocalHashCode = nextHashCode(), //INITIAL_CAPACITY爲16,因此(INITIAL_CAPACITY - 1)的二進制形式爲1111, //與(INITIAL_CAPACITY - 1)進行位與運算就是至關於threadLocalHashCode對16取模 //這是由於Entry數組是一個長度爲16的數組圓環,而key的落腳點便是在這個HashCode對16取模的值 //i就是當前這個key在Entry環形數組的索引值 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //將ThreadLocal和value值構建成一個Entry,放置在ENtry數組中, table[i] = new Entry(firstKey, firstValue); //由於是構造方法,這裏確定是第一次存入數據,因此size爲1 size = 1; //設置entry數組的閾值,閾值爲當前Entry數組長度的三分之二 setThreshold(INITIAL_CAPACITY); } //這個方法是ThreadLocal的方法 private static int nextHashCode() { //nextHashCode爲AtomicInteger類型 //AtomicInteger的getAndAdd()方法就是以用Unsafe的設置方式去更新這個AtomicInteger //更新爲當前值+HASH_INCREMENT return nextHashCode.getAndAdd(HASH_INCREMENT); } //這個方法是AtomicInteger的方法 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //var5即爲當前這個AtomicInteger的值 var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //將AtomicInteger的當前值var5更新爲var5+var4,而war4即爲增量 return var5; } //這個方法是Entry自己的方法 private void setThreshold(int len) { //閾值爲當前entry數組長度的三分之二 threshold = len * 2 / 3; } //ThreadLocalMap的構造方法,參數爲一個ThreadLocalMap private ThreadLocalMap(ThreadLocalMap parentMap) { //獲取參數ThreadLocalMap中的Entry數組 Entry[] parentTable = parentMap.table; //獲取參數Entry數組的長度 int len = parentTable.length; //設置閾值爲數組長度的三分之二 setThreshold(len); //建立一個新的數組,將數組賦值給當前Entry數組table table = new Entry[len]; //循環遍歷 for (int j = 0; j < len; j++) { //獲取參數entry數組的每一個entry節點 Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") //e.get()返回引用referent,這個referent即爲ThreadLocal ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //獲取value Object value = key.childValue(e.value); //對key和value作完基本校驗後,組建新的Entry節點 Entry c = new Entry(key, value); //計算下角標位置 int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) //若是該下角標位置已經有元素了,計算下個索引位置 h = nextIndex(h, len); //直到計算出的索引位置上沒有元素時,將新建的entry放到該索引位置 table[h] = c; //entry數組的元素數量加一 size++; } } } } //當前下角標i的下一個索引位置,若是達到entry數組的長度16的話,從新從0開始 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } }
ThreadLocalMap中維護了一個初始容量爲16的entry數組。這個entry數組就是存儲數據的底層結構,還有一個閾值,看過HashMap底層源碼的就不會對這個概念陌生,另外其實還有一個負載因子,不過這個負載因子並無聲明成員變量,而是在代碼中直接使用的,這個負載因子爲三分之二,咱們能夠看下setThreshold()這個方法,threshold = len * 2 / 3。
繼續往下看,有兩個方法比較重要的,是我們理解ThreadLocalMap數據結構的重要切入點。
//根據當前索引位置和數組長度獲取下一個索引值 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } //根據當前索引位置和數組長度獲取上一個索引值 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }
咱們看nextIndex()方法,噹噹前索引值加1,若是小於數組長度i+1,不然返回0。就是說若是當前索引值加一等於數組的長度就返回0。咱們想到了啥?圓鍾,23點再加一個小時等於24點,24就爲一天的中時數,而24點也是零點,起點。咱們會想到Entry數組是一個環形狀。再看nextIndex()方法,當前索引值減1後若是小於0,返回數組的長度減1,即15,就是i等於0的時候,i減一不是等於負一,而是十五,這個時候咱們能夠確認entry數組就是一個環形結構。使用線性探測法來解決散列衝突的。
下圖即爲Entry數組的結構圖
圖片來源於:https://www.cnblogs.com/micrari/p/6790229.html
Entry數組上每一個節點爲一個Entry,每一個Entry由一個指向ThreadLocal的的弱引用爲key,value即爲咱們設置的變量值。
這裏再想下怎麼經過Key(ThreadLocal)來計算索引值?
這個計算索引值不是經過相似key.hashCode()這種方式來計算的,而是根據類型爲AtomicInteger的nextHashCode成員變量和增量值HASH_INCREMENT成員變量來計算的,計算方式就是經過nextHashCode加上HASH_INCREMENT值的和與Entry數組長度的位與運算來計算的。如代碼所示。
int i = key.threadLocalHashCode & (table.length - 1); private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
理解了Entry數組的數據結構,咱們繼續看ThreadLocalMap提供的主要方法。
獲取:private Entry getEntry(ThreadLocal<?> key)
//根據key值獲取Entry節點 private Entry getEntry(ThreadLocal<?> key) { //根據key值計算索引位置 int i = key.threadLocalHashCode & (table.length - 1); //獲取entry數組中該索引位置的Entry節點 Entry e = table[i]; if (e != null && e.get() == key) //若是e不爲null而且e的Reference(ThreadLocal)與key相同,直接返回e節點 return e; else //若是根據計算出的索引值沒有找到Entry節點 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { //若是e不爲null //獲取entry的key,即ThreadLocal ThreadLocal<?> k = e.get(); if (k == key) //若是和key相等直接返回該元素 return e; if (k == null) //若是k爲null,清理無效的entry,或者說清理ThreadLocal已經被回收的entry expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } //若是e爲null,就直接返回null了 return null; } //該方法主要作了兩件事 //第一將索引爲staleSlot的節點entry的value置爲null,而且將entry置爲null,有利於垃圾回收 //第二從索引stateSlot的下一個索引處開始遍歷判斷每一個entry的ThreadLocal是否爲null,若是爲null,將 //該entry的value和entry自己置爲null,若是不爲null,進行rehash從新計算索引值,判斷從新計算出來的 //索引值和當前循環的索引值是否相等,若是相等,進入下一個循環,若是不等,在環形索引中尋找爲節點爲空的 //下角標,將e節點放置在這個索引位置 private int expungeStaleEntry(int staleSlot) { //獲取ThreadLocalMap的entry數組和數組的長度 Entry[] tab = table; int len = tab.length; //由於在getEntryAfterMiss方法中已經斷定k==null了 //既然key爲null,因此顯示將key對應的value置爲null tab[staleSlot].value = null; //顯示將這個節點entry也置爲null,置爲null有助於垃圾回收 tab[staleSlot] = null; //entry數組的元素個數減一 size--; //執行Rehash直到再次遇到null值 Entry e; int i; //循環遍歷,i的初始值爲當前下角標stateSlot的下一個索引位置 for (i = nextIndex(staleSlot, len); //將entry數組中下角標爲當前遍歷的角標i的節點賦值給e (e = tab[i]) != null; //每循環完一次去獲取下一個索引位置賦值給i i = nextIndex(i, len)) { //獲取當前遍歷的entry的key值,即ThreadLocal ThreadLocal<?> k = e.get(); if (k == null) { //若是key(threadLocal)爲null,即把key對應的value和當前這個節點都置爲null //有助於垃圾回收 e.value = null; tab[i] = null; size--; } else { //若是key不爲null //計算索引值 int h = k.threadLocalHashCode & (len - 1); if (h != i) { //若是新計算的索引值跟如今遍歷的索引值不相等 //將當前遍歷的索引值對應的節點置爲null tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. //在環形索引中尋找爲節點爲空的下角標,將e節點放置在這個索引位置 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
ThreadLocalMap經過key(ThreadLocal)來獲取Entry節點,首先經過key來計算索引值,再經過索引值獲取到某個Entry。若是Entry的key與參數key相同,則直接返回這個Entry節點;若是Entry爲null,則直接返回null;若是Entry不爲null,可是key不相同,就走getEntryAfterMiss()這個方法。這個方法裏面主要是判斷entry的key(ThreadLocal)。若是key既不相等也不爲null,循環遍歷下個索引值對應的entry。可是若是key爲null,這個時候會走expungeStaleEntry()方法了,這個方法比較重要,咱們單獨來講說。
首先咱們想象key爲null表明着什麼?key爲threadLocal,即threadLocal爲null,而threadLocal爲弱引用指向的,其實這裏表示爲ThreadLocal被回收了,雖然ThreadLocal被回收了,可是key對應的value是跟Thread掛鉤的,value可能還沒被回收,因此這裏咱們須要顯示的將value和entry置爲null,以便於垃圾回收這些對象,同時防止內存泄露。不只如此代碼中還會開始遍歷該entry索引後面的整個Entry數組,若是那個entry的key爲null,都會顯示將object和entry置爲null,讓其被回收,防止內存泄露。
設置值:private void set(ThreadLocal<?> key , Object value)
private void set(ThreadLocal<?> key, Object value) { //獲取entry數組和數組的長度 Entry[] tab = table; int len = tab.length; //計算key值對應的索引位置 int i = key.threadLocalHashCode & (len-1); //根據計算的索引值獲取對應的Entry,從該索引處開始循環向後遍歷 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //根據Entry獲取ThreadLocal ThreadLocal<?> k = e.get(); if (k == key) { //若是key與當前entry的key相同 //直接用參數value覆蓋entry中的原value e.value = value; return; } if (k == null) { //若是k爲null //替換無效的entry replaceStaleEntry(key, value, i); return; } } //建立一個新的Entry節點 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) //若是元素個數大於或者等於閾值,擴容 rehash(); } private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; //向索引staleSlot的前面開始循環遍歷,直到tab[i]不爲null //向前遍歷找到最近的一個ThreadLocal爲null的entry for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) //若是entry的key(ThreadLocal)爲null //獲取entry的索引值 slotToExpunge = i; //向staleSlot的後面遍歷 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { //若是entry的key等於參數key //直接覆蓋entry的value值 e.value = value; //?? tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); } private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
終於看完ThreadLocalMap了,咱們能夠接着看ThreadLocal的代碼了!。
protected T initialValue() { return null; }
設置,void set(T value);
public void set(T value) { //獲取當前線程 Thread t = Thread.currentThread(); //獲取當前線程的TreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) //若是ThreadLocalMap不爲null,直接調用ThreadLocalMap的set方法 map.set(this, value); else //若是ThreadLocalMap爲,以當前線程和value值建立ThreadLocalMap createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { //建立ThreadLocalMap並用當前線程指向該map t.threadLocals = new ThreadLocalMap(this, firstValue); }
從set()方法能夠看出每一個線程(Thread)有一個threadLocals變量,如代碼所示:
//Thread類的成員變量 ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal在設置值的時候,會先判斷當前線程有沒有初始化ThreadLocalMap,若是沒有,先根據當前thredLocal(key)和value值生成ThreadLocalMap,並用該線程的成員變量threadLocals指向這個ThreadLocalMap;若是當前線程已經關聯ThreadLocalMap了,則直接經過ThreadLocalMap的set方法設置值。
獲取:T get();
public T get() { //獲取當前線程 Thread t = Thread.currentThread(); //獲取當前線程關聯的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //若是ThreadLocalMap不爲null,根據key(ThreadLocal)值獲取entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; //獲取entry的value值返回 return result; } } //不然初始化當前線程的ThreadLocalMap,value爲null return setInitialValue(); } private T setInitialValue() { //value爲空 T value = initialValue(); //獲取當前線程 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } protected T initialValue() { return null; }
到此,ThreadLocal的主要代碼就介紹完了。
ThreadLocal是否存在內存泄露問題?
會,咱們先來看下ThreadLocal的引用和數據結構圖,圖片來源於:http://www.importnew.com/22039.html,map指的ThreadLocalMap,實線表明強引用,虛線表明弱引用。
咱們看到ThreadLocal有一個強引用和一個弱引用,強引用來自高層代碼中的引用,好比ThreadLocal tl = new TheadLocal(),tl這就是一個強引用,而弱應用來自於ThreadLocalMap中的Entry的key的引用。當高層代碼中把threadlocal實例置爲null之後,就沒有任何強引用指向threadlocal實例,而只有一個弱引用去指向ThreadLocal,可是咱們知道弱引用指向的對象在GC時是會被回收的,因此threadlocal將會被gc回收。這也是Entry中的key使用弱應用的緣由,不然TreadLoca就算在高層代碼中釋放引用後,由於Entry還存在,key仍然指向ThreadLocal,因此讓不會被回收,容易形成內存泄露。
當ThreadLocal被回收後,咱們的value還不能回收,由於存在一條從current thread鏈接過來的強引用.,只要thread存在,這個引用就會一直存在,只有當thread結束之後, current thread纔會被銷燬,強引用纔會斷開, 此時Current Thread, Map, value才能所有被GC回收。
因此這裏存在一個風險就是,在current Thread到銷燬的這段時間內,存在因爲value值過多或者過大致使的內存泄露問題,咱們在想下,若是咱們是使用的線程池,出現什麼結果,線程用完後,直接放回線程池中,不會被銷燬,那麼那些value就會一直存在,這樣產生內存泄露的可能性大大增長。
JDK是怎麼解決這個問題的呢?
咱們回過頭來在看看ThreadLocalMap的set和get方法,咱們發現代碼裏都會循環遍歷Entry數組,檢查entey中的key(ThreadLocal)是否爲null,若是爲null,會顯示的將entry的value和entry自己置爲null,這樣以便entry和entry的value能被GC回收,防止內存泄露。
既然知道了內存泄露的來龍去脈,咱們在使用TheadLocal時候就要特別注意這方面的問題,好比咱們再用完TheadLocal後記得用remove()方法去清除數據。