您好,登錄后才能下訂單哦!
如何進行ThreadLocal源碼分析,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
ThreadLocal是線程本地變量,ThreadLocal為每一個線程創建一個單獨的變量副本ThreadLocalMap,所以每個線程修改自己變量副本不會影響其它的線程。區別于線程同步,我們知道線程同步是為了解決多線程下共享變量的安全問題,而ThreadLocal是為了解決線程內部數據傳遞問題。一個線程內部可以有多個ThreadLocal,但是它門維護線程的同一個ThreadLocalMap變量,共用同一個Entry數組。
ThreadLocal數據結構:
每個線程內部有一個ThreadLocalMap屬性,ThreadLocal通過維護該屬性來保證單個線程內部數據共享。ThreadLocalMap內部有一個entry數組,該數組是key,value型結構,key為當前ThreadLocal的弱引用,value用于存放具體的值,類型為一個泛型結構,支持各種數據變量。ThredLocalMap內Entry數組的下標值也是通過 key.threadLocalHashCode & (數組長度 - 1)來確定的,只不過這個threadLocalHashCode 是通過AutomicLong每次遞增0x61c88647來確定的,這可以盡量減少hash碰撞。不同于HashMap,ThreadLocalMap內部只維護了一個Entry數組,所以當發生hash沖突的時候,ThreadLocalMap會將發生hash沖突的Entry放在當前key對應數組下標后面第一個為空的數組槽位內。ThreadLocal的擴容閾值默認為數組大小的 2/3。因為Entry的key為當前threadlcoal的弱引用,所以在發生gc的時候容易導致key被回收,但是此時value為強引用,所以這種情況會導致內存溢出。但是,當我們調用threadlocal的set,get,remove方法的時候,ThreadLocalMap內都會發生回收過期key的操作,不過這種回收是一種抽樣回收,可能并不能回收所有的過期key。而且在執行set方法回收的時候,可能發生擴容,這時候的擴容判斷是當前數組的長度的1/2。Entry數組默認初始化長度為16。
public class ThreadLocalTest { private static final ThreadLocal<String> threadLocal = new ThreadLocal(); private static String str = null; public static void print1() { System.out.println("打印方法1輸出:" + threadLocal.get()); } public static void print2() { System.out.println("打印方法2輸出:" + str); } public static void main(String[] args) { //線程1 new Thread(() -> { threadLocal.set("線程1設置的str1"); str = "線程1設置的str2"; //睡5秒鐘 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //睡5秒鐘后打印,此時第2個線程早已執行完 print1(); print2(); }).start(); //線程2 new Thread(() -> { threadLocal.set("線程2設置的str1"); str = "線程2設置的str2"; //直接打印 print1(); print2(); }).start(); }}
運行結果:
打印方法1輸出:線程2設置的str1打印方法2輸出:線程2設置的str2打印方法1輸出:線程1設置的str1打印方法2輸出:線程2設置的str2
根據運行結果分析出,使用ThreadLocal的存儲的變量在多線程不存在線程安全問題,常規創建的屬性在多線程下存在線程安全問題。
ThreadLocal中使用了斐波那契散列法,來保證哈希表的離散度。可以保證 nextHashCode 生成的哈希值,均勻的分布在 2 的冪次方上。具體的數學問題不在這里深究。
private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger();//十進制1640531527=0.618*2^32,這個值是黃金分割率*2^32private static final int HASH_INCREMENT = 0x61c88647;//每次調用該方法,hashcode值就會遞增HASH_INCREMENTprivate static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
//用于計算數組下標的值,table.length - 1轉二進制有N個1,那么//key.threadLocalHashCode & (table.length - 1)的值就是threadLocalHashCode的低N位int i = key.threadLocalHashCode & (table.length - 1);
public void set(T value) { //獲取當前線程 Thread t = Thread.currentThread(); //根據當前線程獲取ThreadLocalMap ThreadLocalMap map = getMap(t); //如果map為空則創建一個,否則設置屬性值 if (map != null) //key為當前thread的引用則設置該值 map.set(this, value); else //map為空則創建當前線程的ThreadMap并和當前線程綁定 createMap(t, value);}
private void set(ThreadLocal<?> key, Object value) { //將初始化后的當前數組賦值給臨時數組tab Entry[] tab = table; //獲取當前臨時tab數組長度 int len = tab.length; //計算當前key對應的數組下標 int i = key.threadLocalHashCode & (len-1); //從當前下標開始循環往后遍歷,如果當前數組槽為空,則直接跳出循環,如果不為空,則進行key的判斷 //因為ThreadLocalMap的結構只是數組,沒有鏈表,當key發生沖突, //不同的key定位到相同的數組下標的時候,會往后尋找第一個下標為null //的槽或者第一個key位過期key的槽,并將entry放入并賦值 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //對應下標為i的槽位為空的時候才會走到循環里面的邏輯 //獲取key ThreadLocal<?> k = e.get(); //CASE1:如果key相同,替換value并跳出循環 if (k == key) { e.value = value; return; } //CASE2:如果key為空,說明key已經過期了,當前下標對應的槽可以被使用 if (k == null) { //替換過期key的邏輯 replaceStaleEntry(key, value, i); return; } } //如果當前下標下的數組槽為空,占用該槽位并賦值 tab[i] = new Entry(key, value); //遞增數組大小 int sz = ++size; //沒有清理到數據,且size大小達到了擴容閾值 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}
給當前key找數組槽位的時候,找到的下標對應的key為過期的key的時候,執行替換操作
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { //數組列表 Entry[] tab = table; //數組長度 int len = tab.length; //臨時變量 Entry e; //需要清理的數據的開始下標,默認為當前staleSlot int slotToExpunge = staleSlot; //從當前staleSlot向前查找,找對應數組槽下的entry,直到碰到空的槽則退出循環 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) //如果在查找過程中,碰到key為過期key的情況,更新需要清理的數據的開始下標 if (e.get() == null) slotToExpunge = i; //從當前staleSlot向后查找,找對應數組槽下的entry,直到碰到空的槽則退出循環 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { //獲取當前元素的key ThreadLocal<?> k = e.get(); //如果key相同,則替換value,遷移數據位置 if (k == key { e.value = value; //將過期的tab[staleSlot]放到找到的i下標下 tab[i] = tab[staleSlot]; //當前staleSlot下標下的槽替換為當前的entry,數據的位置被優化了 tab[staleSlot] = e; //條件成立說明向前過程中并沒有找到過期的key if (slotToExpunge == staleSlot) //修改需要清理數據的開始下標為替換數據后的下標 slotToExpunge = i; //清理數據 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //k==null說明循環過程中未找到匹配的key //slotToExpunge == staleSlot說明向前遍歷過程中未找到過期的key if (k == null && slotToExpunge == staleSlot) //可以將循環向后查找的i指向slotToExpunge,因為在向后查找的過程中沒有找到相同的key //該段期間沒必要處理了 slotToExpunge = i; } //走到這里說明循環向后查找的過程中,沒有找到相同的key //直接使用當前下標并賦值 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); //條件成立,說明在向前向后遍歷中,slotToExpunge被改變了 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
為什么有while ( (n >>>= 1) != 0),這樣不是可能清理不了所有數據嗎?是的,ThreadLocal的設計行就是部分清除,類似于抽樣,避免清理所有影響性能。
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(); if (size >= threshold - threshold / 4) resize();}
擴容邏輯,比較簡單,數組變大兩倍,舊數據遷移到新數組,如果key已經過期的,則直接將value也設置為空。這里需要注意的時候,清理過程中擴容的閾值是原數組容量的 1/2, size >= threshold - threshold / 4,我們直到threashold = 2 / 3 * length, 所以轉化后size >= 3 / 4 * (2 / 3) * length。
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(); //如果對應的key已經回收 if (k == null) { //value設置為空 e.value = null; // Help the GC } else { //進行數據遷移,如果存在沖突,則放到計算出來的下標的后方第一個不為null的槽 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } //重新設置擴容閾值 setThreshold(newLen); size = count; table = newTab;}
當我們調用threadLocal的get方法的時候,首先會調用getMap方法,該方法根據當前線程獲取當前線程的ThreadLocal.ThreadLocalMap threadLocals屬性,如果非空,再獲取對應的ThreadLocal的ThreadLocalMap 里面的entry,根據entry獲取對應的value,這個過程會調用expungestaleEntry方法,清空key為空的hash槽的值,并將key不為空的且通過key的hash值計算出來的下標發生過向后偏移的entry移動到更靠近計算出來的下標值的后面的某個空的槽內。如果getMap返回空,說明我們可能沒用調用ThreadLocal的set方法的情況下調用了get方法,那么創建一個ThreadLocalMap,初始化entry數組,設置擴容閾值,并設置對應的ThreadLocal的hash槽的值為空。
public T get() { //獲取當前線程 Thread t = Thread.currentThread(); //取出當前線程的ThreadLocalMap屬性 ThreadLocalMap map = getMap(t); //如果當前線程的ThreadLocalMap不為空 if (map != null) { //獲取ThreadLocalMap的Entry數組 ThreadLocalMap.Entry e = map.getEntry(this); //如果數組不為空,取出value值返回 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();}
//獲取thread的threadLocals屬性ThreadLocalMap getMap(Thread t) { return t.threadLocals;}
//獲取ThreadLocalMap的entry數組對應下標的數據private Entry getEntry(ThreadLocal<?> key) { //計算下標 int i = key.threadLocalHashCode & (table.length - 1); //獲取對應下標數據 Entry e = table[i]; if (e != null && e.get() == key) return e; //如果取不到,為什么有這種情況? //從put方法中我們知道,threadlocalMap不同于hashMap //內部只有數組,數組的每個hash槽下只有一個entry值 //如果在put的時候發現對應hash槽的值不為空,且key不相同 //則往后找第一個為空的hash槽,講entry放入該hash槽 else return getEntryAfterMiss(key, i, e);}
//從對應下標往后循環查找,這里有個特殊的地方nextIndex//該方法:從對應下標往后循環返回下標,如果超出數組長度,//則從0下標開始繼續往后循環返回下標private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //循環遍歷 while (e != null) { ThreadLocal<?> k = e.get(); //case1:key值相同,返回對應的entry if (k == key) return e; //case2:發現對應entry數組下標下的key為空,清理 if (k == null) expungeStaleEntry(i); //case3:key不為空但key不相同,數組下標往后推進 else i = nextIndex(i, len); //返回下一個下標值對應的entry e = tab[i]; } return null;}
//從對應下標往后循環,如果超出數組長度,則從0下標開始繼續往后循環//返回具體下標值private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0);}
從當前staleSlot開始循環清理過期key對應的entry數組內的值;如果key不為空且當前線程對應的threadlocal的hash值計算出來的下標發生過遷移,說明之前在put的時候,在對應下標下發生過hash沖突,將當前下標下的entry數組對應的值置為null,并將當前下標下的entry值移動到更接近通過hash值計算出來的下標之后的某個空的槽中。循環在進行下標右移的過程中,如果碰到對應下標下的槽數據為空,則退出循環。該方法在執行的時候會將本該在staleSlot位置的key對應的變量移動到該位置或更靠近該位置的后方。避免remove方法遍歷的時候出現null導致清理不到的情況。
private int expungeStaleEntry(int staleSlot) { //將全局entry數組賦值給臨時tab Entry[] tab = table; //臨時entry數組當前長度 int len = tab.length; //設置對應數組下標下的entry的value為空 tab[staleSlot].value = null; //設置對應entry為空 tab[staleSlot] = null; //entry數組全局長度-1 size--; Entry e; int i; //從當前下標往后循環遍歷,直到對應的下標下槽內數據為空跳出循環 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { //獲取對應下標下當前entry對應的key ThreadLocal<?> k = e.get(); //如果key為空則清理entry的value和設置當前數組對應entry為空 if (k == null) { e.value = null; tab[i] = null; size--; //如果key不為空 } else { //計算獲取對應的下標,這個本該是存放entry的位置,但是可能由于hash沖突,put的時候向后偏移了 int h = k.threadLocalHashCode & (len - 1); //條件成立說明在put的時候計算出來的下標發生過hash沖突 //數據向后偏移過,而且 h < i if (h != i) { //將當前下標下entry設置為空 tab[i] = null; //從計算出來的下標h循環向后獲取一個對應entry為空的下標值 //該下標下存放當前entry while (tab[h] != null) //這個新計算出來的h的值更靠近計算獲取的下標 h = nextIndex(h, len); //將entry放在對應下標 tab[h] = e; } } //返回進行處理過后的起點下標i return i;}
private T setInitialValue() { //獲取一個空值 T value = initialValue(); Thread t = Thread.currentThread(); //獲取當前線程的ThreadMap ThreadLocalMap map = getMap(t); //如果不為空,則將當前空值注入 if (map != null) map.set(this, value); else //否則創建這個ThreadMap并和當前Thread綁定 createMap(t, value); return value;}
remove方法也很簡單,就是將key的引用設置為null,然后找到key所對應的數組槽位,執行清理操作。
在ThreadLocal使用完畢后,執行remove方法防止內存溢出。
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(); expungeStaleEntry(i); return; } }}
public void clear() { this.referent = null;}
上面說完了ThreadLocal的問題,可以看出,ThreadLocal只能在單個線程內部傳遞參數,無法在子父線程間傳遞參數。
但是InheritableThreadLocal的出現解決了這個問題。
public class InheriTableThreadLocalTest { private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { threadLocal.set("主線程設置值"); new Thread(() -> { System.out.println(threadLocal.get()); }).start(); }}
分析InheritableThreadLocal類,發現繼承于ThreadLocal,但是在createMap,getMap的時候維護的是inheritableThreadLocals
public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }}
在線程初始化的代碼init方法中,有這么一段邏輯:
如果父線程的inheritThreadLocals不為空,則調用ThreadLocal.createInheritedMap方法,該方法傳遞了父線程的inheritableThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
再看看ThreadLocal.createInheritedMap方法,子線程在創建的時候,將父線程的inheritableThreadLocals復制了過來保存在了自己的inheritableThreadLocals中。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap);}
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } }}
看完上述內容,你們掌握如何進行ThreadLocal源碼分析的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。