摘要:什么是,簡單翻譯過來就是本地線程,但是直接這么翻譯很難理解的作用,如果換一種說法,可以稱為線程本地存儲。魔數的選取和斐波那契散列有關,對應的十進制為。而斐波那契散列的乘數可以用如果把這個值給轉為帶符號的,則會得到。
什么是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 ThreadLocallocal=new ThreadLocal (){ 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是如何實現多線程場景下的共享變量副本隔離?
帶著疑問,來看一下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中 else createMap(t, value); //說明當前線程是第一次使用線程本地變量,構造map }
Thread.currentThread 獲取當前執行的線程
getMap(t) ,根據當前線程得到當前線程的ThreadLocalMap對象,這個對象具體是做什么的?稍后分析
如果map不為空,說明當前線程已經構造過ThreadLocalMap,直接將值存儲到map中
如果map為空,說明是第一次使用,調用createMap構造
ThreadLocalMap是什么?我們來分析一下這句話,ThreadLocalMap map=getMap(t)獲得一個ThreadLocalMap對象,那這個對象是干嘛的呢?
其實不用分析,基本上也能猜測出來,Map是一個集合,集合用來存儲數據,那么在ThreadLocal中,應該就是用來存儲線程的局部變量的。ThreadLocalMap這個類很關鍵。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
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對象用來真正存儲數據
static class ThreadLocalMap { static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } } ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { //構造一個Entry數組,并設置初始大小 table = new Entry[INITIAL_CAPACITY]; //計算Entry數據下標 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //將`firstValue`存入到指定的table下標中 table[i] = new Entry(firstKey, firstValue); size = 1;//設置節點長度為1 setThreshold(INITIAL_CAPACITY); //設置擴容的閾值 } //...省略部分代碼 }
分析到這里,基本知道了ThreadLocalMap長啥樣了,也知道它是如何構造的?那么我看到這里的時候仍然有疑問
Entry集成了WeakReference,這個表示什么意思?
在構造ThreadLocalMap的時候new ThreadLocalMap(this, firstValue);,key其實是this,this表示當前對象的引用,在當前的案例中,this指的是ThreadLocal
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)
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
關鍵點就在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); //初始大小16 magicHash(32); //擴容一倍 } private static void magicHash(int size){ int hashCode = 0; for(int i=0;i輸出結果
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相等,覆蓋value if (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進行替換private void replaceStaleEntry(ThreadLocal> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; //向前掃描,查找最前一個無效的slot int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) //通過循環遍歷,可以定位到最前面一個無效的slot slotToExpunge = i; //從i開始往后一直遍歷到數組最后一個Entry(線性探索) for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); //找到匹配的key以后 if (k == key) { e.value = value;//更新對應slot的value值 //與無效的sloat進行交換 tab[i] = tab[staleSlot]; tab[staleSlot] = e; //如果最早的一個無效的slot和當前的staleSlot相等,則從i作為清理的起點 if (slotToExpunge == staleSlot) slotToExpunge = i; //從slotToExpunge開始做一次連續的清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //如果當前的slot已經無效,并且向前掃描過程中沒有無效slot,則更新slotToExpunge為當前位置 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } //如果key對應的value在entry中不存在,則直接放一個新的entry tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); //如果有任何一個無效的slot,則做一次清理 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }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 staleSlot tab[staleSlot].value = null;//刪除value tab[staleSlot] = null;//刪除entry size--; //map的size遞減 // Rehash until we encounter null Entry 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(); //從當前線程中獲取ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //查詢當前ThreadLocal變量實例對應的Entry ThreadLocalMap.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的實際應用場景:
比如在線程級別,維護session,維護用戶登錄信息userID(登陸時插入,多個地方獲取)
數據庫的鏈接對象Connection,可以通過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()方法,清除數據。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72585.html
摘要:導讀閱讀本文需要有足夠的時間,筆者會由淺到深帶你一步一步了解一個資深架構師所要掌握的各類知識點,你也可以按照文章中所列的知識體系對比自身,對自己進行查漏補缺,覺得本文對你有幫助的話,可以點贊關注一下。目錄一基礎篇二進階篇三高級篇四架構篇五擴 導讀:閱讀本文需要有足夠的時間,筆者會由淺到深帶你一步一步了解一個資深架構師所要掌握的各類知識點,你也可以按照文章中所列的知識體系對比自身,對自己...
摘要:阿里巴巴的共享服務理念以及企業級互聯網架構建設的思路,給這些企業帶來了不少新的思路,這也是我最終決定寫這本書的最主要原因。盡在雙阿里巴巴技術演進與超越是迄今唯一由阿里巴巴集團官方出品全面闡述雙八年以來在技術和商業上演進和創新歷程的書籍。 showImg(https://segmentfault.com/img/remote/1460000015386860); 1、大型網站技術架構:核...
摘要:但是還有另外的功能看的后一半代碼作用就是掃描位置之后的數組直到某一個為的位置,清除每個為的,所以使用可以降低內存泄漏的概率。 在涉及到多線程需要共享變量的時候,一般有兩種方法:其一就是使用互斥鎖,使得在每個時刻只能有一個線程訪問該變量,好處就是便于編碼(直接使用 synchronized 關鍵字進行同步訪問),缺點在于這增加了線程間的競爭,降低了效率;其二就是使用本文要講的 Threa...
摘要:前言最近開發公司的項目,遇到了分布式的場景,即,同一條數據可能被多臺服務器或者說多個線程同時修改,此時可能會出現分布式事務的問題,隨即封裝了分布式鎖的注解。 前言 最近開發公司的項目,遇到了分布式的場景,即,同一條數據可能被多臺服務器或者說多個線程同時修改,此時可能會出現分布式事務的問題,隨即封裝了redis分布式鎖的注解。 場景分析 前提:我的銀行卡有0元錢,現在有A,B兩個人,想分...
閱讀 3118·2021-11-15 18:14
閱讀 1773·2021-09-22 10:51
閱讀 3283·2021-09-09 09:34
閱讀 3505·2021-09-06 15:02
閱讀 1013·2021-09-01 11:40
閱讀 3186·2019-08-30 13:58
閱讀 2523·2019-08-30 11:04
閱讀 1081·2019-08-28 18:31