(八)ThreadLocal的使用及原理分析
什么是ThreadLocal
ThreadLocal,簡單翻譯過來就是本地線程,但是直接這么翻譯很難理解ThreadLocal的作用,如果換一種說法,可以稱為線程本地存儲。簡單來說,就是ThreadLocal為共享變量在每個線程中都創建一個副本,每個線程可以訪問自己內部的副本變量。這樣做的好處是可以保證共享變量在多線程環境下訪問的線程安全性
ThreadLocal的使用
沒有使用ThreadLocal時
通過一個簡單的例子來演示一下ThreadLocal的作用,這段代碼是定義了一個靜態的成員變量num,然后通過構造5個線程對這個num做遞增
public class ThreadLocalDemo {private static Integer num=0;public static void main(String[] args) {Thread[] threads=new Thread[5];for(int i=0;i<5;i++){threads[i]=new Thread(()->{num+=5;System.out.println(Thread.currentThread().getName()+" : "+num);},"Thread-"+i);}for(Thread thread:threads){thread.start();}} }運行結果
Thread-0 : 5 Thread-1 : 10 Thread-2 : 15 Thread-3 : 20 Thread-4 : 25每個線程都會對這個成員變量做遞增,如果線程的執行順序不確定,那么意味著每個線程獲得的結果也是不一樣的。
使用了ThreadLocal以后
通過ThreadLocal對上面的代碼做一個改動
public class ThreadLocalDemo {private static final ThreadLocal<Integer> local=new ThreadLocal<Integer>(){protected Integer initialValue(){return 0; //通過initialValue方法設置默認值}};public static void main(String[] args) {Thread[] threads=new Thread[5];for(int i=0;i<5;i++){threads[i]=new Thread(()->{int num=local.get().intValue();num+=5;System.out.println(Thread.currentThread().getName()+" : "+num);},"Thread-"+i);}for(Thread thread:threads){thread.start();}} }運行結果
Thread-0 : 5 Thread-4 : 5 Thread-2 : 5 Thread-1 : 5 Thread-3 : 5從結果可以看到,每個線程的值都是5,意味著各個線程之間都是獨立的變量副本,彼此不相互影響.
ThreadLocal會給定一個初始值,也就是initialValue()方法,而每個線程都會從ThreadLocal中獲得這個初始化的值的副本,這樣可以使得每個線程都擁有一個副本拷貝看到這里,估計有很多人都會和我一樣有一些疑問
帶著疑問,來看一下ThreadLocal這個類的定義(默認情況下,JDK的源碼都是基于1.8版本)
從ThreadLocal的方法定義來看,還是挺簡單的。就幾個方法
- get: 獲取ThreadLocal中當前線程對應的線程局部變量
- set:設置當前線程的線程局部變量的值
- remove:將當前線程局部變量的值刪除
另外,還有一個initialValue()方法,在前面的代碼中有演示,作用是返回當前線程局部變量的初始值,這個方法是一個protected方法,主要是在構造ThreadLocal時用于設置默認的初始值
set方法的實現
set方法是設置一個線程的局部變量的值,相當于當前線程通過set設置的局部變量的值,只對當前線程可見。
public void set(T value) {Thread t = Thread.currentThread();//獲取當前執行的線程ThreadLocalMap map = getMap(t); //獲得當前線程的ThreadLocalMap實例if (map != null)//如果map不為空,說明當前線程已經有了一個ThreadLocalMap實例map.set(this, value);//直接將當前value設置到ThreadLocalMap中elsecreateMap(t, value); //說明當前線程是第一次使用線程本地變量,構造map}- Thread.currentThread 獲取當前執行的線程
- getMap(t) ,根據當前線程得到當前線程的ThreadLocalMap對象,這個對象具體是做什么的?稍后分析
- 如果map不為空,說明當前線程已經構造過ThreadLocalMap,直接將值存儲到map中
- 如果map為空,說明是第一次使用,調用createMap構造
ThreadLocalMap是什么?
我們來分析一下這句話,ThreadLocalMap map=getMap(t)獲得一個ThreadLocalMap對象,那這個對象是干嘛的呢?
其實不用分析,基本上也能猜測出來,Map是一個集合,集合用來存儲數據,那么在ThreadLocal中,應該就是用來存儲線程的局部變量的。ThreadLocalMap這個類很關鍵。
t.threadLocals實際上就是訪問Thread類中的ThreadLocalMap這個成員變量
public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null; ... }從上面的代碼發現每一個線程都有自己單獨的ThreadLocalMap實例,而對應這個線程的所有本地變量都會保存到這個map內
ThreadLocalMap是在哪里構造?
在set方法中,有一行代碼createmap(t,value);,這個方法就是用來構造ThreadLocalMap,從傳入的參數來看,它的實現邏輯基本也能猜出出幾分吧
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}Thread t 是通過Thread.currentThread()來獲取的表示當前線程,然后直接通過new ThreadLocalMap將當前線程中的threadLocals做了初始化
ThreadLocalMap是一個靜態內部類,內部定義了一個Entry對象用來真正存儲數據
- Entry集成了WeakReference,這個表示什么意思?
- 在構造ThreadLocalMap的時候new ThreadLocalMap(this, firstValue);,key其實是this,this表示當前對象的引用,在當前的案例中,this指的是ThreadLocal<Integer> local。那么多個線程對應同一個ThreadLocal實例,怎么對每一個ThreadLocal對象做區分呢?
解惑WeakReference
weakReference表示弱引用,在Java中有四種引用類型,強引用、弱引用、軟引用、虛引用。
使用弱引用的對象,不會阻止它所指向的對象被垃圾回收器回收。
在Java語言中, 當一個對象o被創建時, 它被放在Heap里. 當GC運行的時候, 如果發現沒有任何引用指向o, o就會被回收以騰出內存空間. 也就是說, 一個對象被回收, 必須滿足兩個條件:
- 沒有任何引用指向它
- GC被運行.
這段代碼中,構造了兩個對象a,b,a是對象DemoA的引用,b是對象DemoB的引用,對象DemoB同時還依賴對象DemoA,那么這個時候我們認為從對象DemoB是可以到達對象DemoA的。這種稱為強可達(strongly reachable)
DemoA a=new DemoA(); DemoB b=new DemoB(a);如果我們增加一行代碼來將a對象的引用設置為null,當一個對象不再被其他對象引用的時候,是會被GC回收的,但是對于這個場景來說,即時是a=null,也不可能被回收,因為DemoB依賴DemoA,這個時候是可能造成內存泄漏的
DemoA a=new DemoA(); DemoB b=new DemoB(a); a=null;通過弱引用,有兩個方法可以避免這樣的問題
//方法1 DemoA a=new DemoA(); DemoB b=new DemoB(a); a=null; b=null; //方法2 DemoA a=new DemoA(); WeakReference b=new WeakReference(a); a=null;對于方法2來說,DemoA只是被弱引用依賴,假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除所有指向該對象的弱引用,然后把這個弱可達對象標記為可終結(finalizable)的,這樣它隨后就會被回收。
試想一下如果這里沒有使用弱引用,意味著ThreadLocal的生命周期和線程是強綁定,只要線程沒有銷毀,那么ThreadLocal一直無法回收。而使用弱引用以后,當ThreadLocal被回收時,由于Entry的key是弱引用,不會影響ThreadLocal的回收防止內存泄漏,同時,在后續的源碼分析中會看到,ThreadLocalMap本身的垃圾清理會用到這一個好處,方便對無效的Entry進行回收解惑ThreadLocalMap以this作為key
在構造ThreadLocalMap時,使用this作為key來存儲,那么對于同一個ThreadLocal對象,如果同一個Thread中存儲了多個值,是如何來區分存儲的呢?
答案就在firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
關鍵點就在threadLocalHashCode,它相當于一個ThreadLocal的ID,實現的邏輯如下
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode =new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT); }這里用到了一個非常完美的散列算法,可以簡單理解為,對于同一個ThreadLocal下的多個線程來說,當任意線程調用set方法存入一個數據到Entry中的時候,其實會根據threadLocalHashCode生成一個唯一的id標識對應這個數據,存儲在Entry數據下標中。
- threadLocalHashCode是通過nextHashCode.getAndAdd(HASH_INCREMENT)來實現的
i*HASH_INCREMENT+HASH_INCREMENT,每次新增一個元素(ThreadLocal)到Entry[],都會自增0x61c88647,目的為了讓哈希碼能均勻的分布在2的N次方的數組里
- Entry[i]= hashCode & (length-1)
魔數0x61c88647
從上面的分析可以看出,它是在上一個被構造出的ThreadLocal的threadLocalHashCode的基礎上加上一個魔數0x61c88647。我們來做一個實驗,看看這個散列算法的運算結果
private static final int HASH_INCREMENT = 0x61c88647;public static void main(String[] args) {magicHash(16); //初始大小16magicHash(32); //擴容一倍}private static void magicHash(int size){int hashCode = 0;for(int i=0;i<size;i++){hashCode = i*HASH_INCREMENT+HASH_INCREMENT;System.out.print((hashCode & (size-1))+" ");}System.out.println();}輸出結果
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0根據運行結果,這個算法在長度為2的N次方的數組上,確實可以完美散列,沒有任何沖突, 是不是很神奇。
魔數0x61c88647的選取和斐波那契散列有關,0x61c88647對應的十進制為1640531527。而斐波那契散列的乘數可以用(long) ((1L << 31) * (Math.sqrt(5) - 1)); 如果把這個值給轉為帶符號的int,則會得到-1640531527。也就是說(long) ((1L << 31) * (Math.sqrt(5) - 1));得到的結果就是1640531527,也就是魔數0x61c88647 //(根號5-1)*2的31次方=(根號5-1)/2 *2的32次方=黃金分割數*2的32次方 long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1)); System.out.println("32位無符號整數: " + l1); int i1 = (int) l1; System.out.println("32位有符號整數: " + i1); 總結,我們用0x61c88647作為魔數累加為每個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,得到的結果分布很均勻。
圖形分析
為了更直觀的體現set方法的實現,通過一個圖形表示如下
set剩余源碼分析
前面分析了set方法第一次初始化ThreadLocalMap的過程,也對ThreadLocalMap的結構有了一個全面的了解。那么接下來看一下map不為空時的執行邏輯
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;// 根據哈希碼和數組長度求元素放置的位置,即數組下標int i = key.threadLocalHashCode & (len-1);//從i開始往后一直遍歷到數組最后一個Entry(線性探索)for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//如果key相等,覆蓋valueif (k == key) {e.value = value;return;}//如果key為null,用新key、value覆蓋,同時清理歷史key=null的陳舊數據if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;//如果超過閥值,就需要擴容了if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}主要邏輯
- 根據key的散列哈希計算Entry的數組下標
- 通過線性探索探測從i開始往后一直遍歷到數組的最后一個Entry
- 如果map中的key和傳入的key相等,表示該數據已經存在,直接覆蓋
- 如果map中的key為空,則用新的key、value覆蓋,并清理key=null的數據
- rehash擴容
replaceStaleEntry
由于Entry的key為弱引用,如果key為空,說明ThreadLocal這個對象被GC回收了。
replaceStaleEntry的作用就是把陳舊的Entry進行替換
cleanSomeSlots
這個函數有兩處地方會被調用,用于清理無效的Entry
- 插入的時候可能會被調用
- 替換無效slot的時候可能會被調用
區別是前者傳入的n為元素個數,后者為table的容量
private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {// i在任何情況下自己都不會是一個無效slot,所以從下一個開始判斷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;}expungeStaleEntry
執行一次全量清理
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlottab[staleSlot].value = null;//刪除valuetab[staleSlot] = null;//刪除entrysize--; //map的size遞減// Rehash until we encounter nullEntry e;int i;for (i = nextIndex(staleSlot, len);// 遍歷指定刪除節點,所有后續節點(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {//key為null,執行刪除操作e.value = null;tab[i] = null;size--;} else {//key不為null,重新計算下標int h = k.threadLocalHashCode & (len - 1);if (h != i) {//如果不在同一個位置tab[i] = null;//把老位置的entry置null(刪除)// 從h開始往后遍歷,一直到找到空為止,插入while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}get操作
set的邏輯分析完成以后,get的源碼分析就很簡單了
public T get() {Thread t = Thread.currentThread();//從當前線程中獲取ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//查詢當前ThreadLocal變量實例對應的EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {//獲取成功,直接返回@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果map為null,即還沒有初始化,走初始化方法return setInitialValue();}setInitialValue
根據initialValue()的value初始化ThreadLocalMap
private T setInitialValue() {T value = initialValue();//protected方法,用戶可以重寫Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)//如果map不為null,把初始化value設置進去map.set(this, value);else//如果map為null,則new一個map,并把初始化value設置進去createMap(t, value);return value;}- 從當前線程中獲取ThreadLocalMap,查詢當前ThreadLocal變量實例對應的Entry,如果不為null,獲取value,返回
- 如果map為null,即還沒有初始化,走初始化方法
remove方法
remove的方法比較簡單,從Entry[]中刪除指定的key就行
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}private void remove(ThreadLocal<?> key) {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)]) {if (e.get() == key) {e.clear();//調用Entry的clear方法expungeStaleEntry(i);//清除陳舊數據return;}}}應用場景
ThreadLocal的實際應用場景:
問題
ThreadLocal的內存泄漏ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,如果一個ThreadLocal沒有外部強引用,當系統執行GC時,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現一個key為null的Entry,而這個key=null的Entry是無法訪問的,當這個線程一直沒有結束的話,那么就會存在一條強引用鏈
Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永遠無法回收而造成內存泄漏
其實我們從源碼分析可以看到,ThreadLocalMap是做了防護措施的- 首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不為null并且key相同則返回e
- 如果e為null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值為null,則擦除該位置的Entry,否則繼續向下一個位置查詢
在這個過程中遇到的key為null的Entry都會被擦除,那么Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究代碼可以發現,set操作也有類似的思想,將key為null的這些Entry都刪除,防止內存泄露。
但是這個設計一來與一個前提條件,就是調用get或者set方法,但是不是所有場景都會滿足這個場景的,所以為了避免這類的問題,我們可以在合適的位置手動調用ThreadLocal的remove函數刪除不需要的ThreadLocal,防止出現內存泄漏
- 將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然后remove它,防止內存泄露
- 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
總結
以上是生活随笔為你收集整理的(八)ThreadLocal的使用及原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 转:AMS之dumpsys篇
- 下一篇: [Wrong Answer] Leetc
