日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ThreadLocal原理及用法详解

發布時間:2025/1/21 编程问答 17 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ThreadLocal原理及用法详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

背景

一直以來對ThreadLocal用法模棱兩可,不知道怎么用今天好好研究了下給大家分享下。

  • 1、講解ThreadLocal之前先回顧下什么是取模、x^y、弱引用。

1. 取模運算實際上是計算兩數相除以后的余數.

假設a除以b的商是c,d是相對應的余數,那么幾乎所有的計算機系統都滿足a = c* b + d。所以d=a-b*c。
?17 % 10 的計算結果如下:d = (17) - (17 / 10) x 10 = (17) - (1 x 10) = 7
?-17 % 10 的計算結果如下:d = (-17) - (-17 / 10) x 10 = (-17) - (-1 x 10) = -7
-17 % -10 的計算結果如下:d = 17 - (17 / -10) x (-10) = (17) - (-1 x -10) = 7
-17 % -10 的計算結果如下:d= (-17) - (-17 / -10) x (-10) = (-17) - (1 x -10) = -7
可以看出:運算結果的符號始終和被模數的符號一致。

2. x^y 按位取異或。

如:x是二進制數0101 y是二進制數1011 則結果為x^y=1110,0^1=1,0^0=0,1^1=0,1^0=1!只要有一個為1就取值為1。

3. 弱引用

弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。這里所說的被弱引用關聯的對象是指只有弱引用與之關聯,如果存在強引用同時與之關聯,則進行垃圾回收時也不會回收該對象。

  • 2、我們要問兩個問題

1.什么是ThreadLocal
ThreadLocal從字面意識可以理解為線程本地變量。也就是說如果定義了ThreadLocal,每個線程往這個ThreadLocal中讀寫是線程隔離,互相之間不會影響的。它提供了一種將可變數據通過每個線程有自己的獨立副本從而實現線程封閉的機制。
2.實現的思路是什么
Tread類有一個類型為ThreadLocal.ThreadLocalMap的實例變量threadLocals,我們使用線程的時候有一個自己的ThreadLocalMap。ThreadLocalMap有自己的獨立實現,可以簡單認為ThreadLocal視為key,value為代碼中放入的值(實際上key并不是ThreadLocal本身,通過源碼可以知道它是一個弱引用)。調用ThreadLocal的set方法時候,都會存到ThreadLocalMap里面。調用ThreadLocal的get方法時候,在自己map里面找key,從而實現線程隔離。

  • 3、ThreadLocal最主要的實現在于ThreadLocalMap這個內部類里面,我們重點關注ThreadLocalMap這個類的用法,看看兩位大師Josh Bloch and Doug Lea是什么設計出如此好的類。

  • 4、ThreadLocalMap

image.png

?

上面是ThreadLocalMap所有的API
ThreadLocalMap提供了一種為ThreadLocal定制的高效實現,并且自帶一種基于弱引用的垃圾清理機制。

  • 存儲結構方面
    存儲結構可以理解為一個map,但是不要和java.util.Map弄混。
  • static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

    從上面代碼可以看出Entry便是ThreadLocalMap里定義的節點,它繼承了WeakReference類,定義了一個類型為Object的value,用于存放塞到ThreadLocal里的值,key可以視為為ThreadLocal。

  • 為什么要用弱引用,因為如果這里使用普通的key-value形式來定義存儲結構,實質上就會造成節點的生命周期與線程強綁定,只要線程沒有銷毀,那么節點在GC分析中一直處于可達狀態,沒辦法被回收,而程序本身也無法判斷是否可以清理節點。弱引用是Java四種引用的的第三種(其它三種強引用、軟引用、虛引用),比軟引用更加弱一些,如果一個對象沒有強引用鏈可達,那么一般活不過下一次GC。當某個ThreadLocal已經沒有強引用可達,則隨著它被垃圾回收,在ThreadLocalMap里對應的Entry的鍵值會失效,這為ThreadLocalMap本身的垃圾清理提供了便利。
  • Entry里面的成員變量和方法
  • /*** 初始容量,必須為2的冪.*/private static final int INITIAL_CAPACITY = 16;/*** 根據需要調整大小。* 長度必須總是2的冪。*/private Entry[] table;/*** 表中條目的數量。*/private int size = 0;/*** 要調整大小的下一個大小值。默認為0*/private int threshold; // Default to 0/*** 將調整大小閾值設置維持最壞2/3的負載因子。*/private void setThreshold(int len) {threshold = len * 2 / 3;}/***上一個索引*/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);}

    由于ThreadLocalMap使用線性探測法來解決散列沖突,所以實際上Entry[]數組在程序邏輯上是作為一個環形存在的。

  • 構造函數
  • /*** Construct a new map initially containing (firstKey, firstValue).* ThreadLocalMaps are constructed lazily, so we only create* one when we have at least one entry to put in it.* 構造一個最初包含(firstKey, firstValue)的新映射threadlocalmap是延遲構造的,因 * 此當我們至少有一個元素可以放進去的時候才去創建。*/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);}

    重點說下這個hash函數int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    ThreadLocal類中有一個被final修飾的類型為int的threadLocalHashCode,它在該ThreadLocal被構造的時候就會生成,相當于一個ThreadLocal的ID,而它的值來源于

    private final int threadLocalHashCode = nextHashCode();/*** The next hash code to be given out. Updated atomically. Starts at* zero.*/private static AtomicInteger nextHashCode =new AtomicInteger();/*** 連續生成的哈希碼之間的區別——循環隱式順序線程本地id以近乎最優的方式展開* 用于兩倍大小表的乘法哈希值。*/private static final int HASH_INCREMENT = 0x61c88647;/*** 返回下一個hashcode*/private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}

    通過理論和實踐算出通,當我們用0x61c88647作為魔數累加為每個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,得到的結果分布很均勻。

    ThreadLocalMap使用的是線性探測法,均勻分布的好處在于很快就能探測到下一個臨近的可用slot,從而保證效率。這就回答了上文拋出的為什么大小要為2的冪的問題。為了優化效率。對于& (INITIAL_CAPACITY - 1),相信有過算法閱讀源碼較多的程序員,一看就明白,對于2的冪作為模數取模,可以用&(2n-1)來替代%2n,位運算比取模效率高很多。至于為什么,因為對2^n取模,只要不是低n位對結果的貢獻顯然都是0,會影響結果的只能是低n位。

  • TreadLocal中的get方法
    ThreadLocal中的get方法會調用這個ThreadLocalMap中的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;elsereturn getEntryAfterMiss(key, i, e);}/*** Version of getEntry method for use when key is not found in* its direct hash slot.** @param key the thread local object* @param i the table index for key's hash code* @param e the entry at table[i]* @return the entry associated with key, or null if no such*/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);elsei = nextIndex(i, len);e = tab[i];}return null;}private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlottab[staleSlot].value = null;tab[staleSlot] = null;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) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;} private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlottab[staleSlot].value = null;tab[staleSlot] = null;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) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}

    這上面的注釋很清楚,這里都不在多講了

    簡單說下當調用get方法時候遇到的情況

    根據入參threadLocal的threadLocalHashCode對表容量取模得到index如果index對應的slot就是要讀的threadLocal,則直接返回結果
    調用getEntryAfterMiss線性探測,過程中每碰到無效slot,調用expungeStaleEntry進行段清理;如果找到了key,則返回結果entry
    沒有找到key,返回null

  • TreadLocal中的set方法
  • /*** Set the value associated with key.** @param key the thread local object* @param value the value to be set*/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)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}/*** Replace a stale entry encountered during a set operation* with an entry for the specified key. The value passed in* the value parameter is stored in the entry, whether or not* an entry already exists for the specified key.** As a side effect, this method expunges all stale entries in the* "run" containing the stale entry. (A run is a sequence of entries* between two null slots.)** @param key the key* @param value the value to be associated with key* @param staleSlot index of the first stale entry encountered while* searching for key.*/private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// Back up to check for prior stale entry in current run.// We clean out whole runs at a time to avoid continual// incremental rehashing due to garbage collector freeing// up refs in bunches (i.e., whenever the collector runs).int slotToExpunge = staleSlot;for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// Find either the key or trailing null slot of run, whichever// occurs firstfor (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// If we find key, then we need to swap it// with the stale entry to maintain hash table order.// The newly stale slot, or any other stale slot// encountered above it, can then be sent to expungeStaleEntry// to remove or rehash all of the other entries in run.if (k == key) {e.value = value;tab[i] = tab[staleSlot];tab[staleSlot] = e;// Start expunge at preceding stale entry if it existsif (slotToExpunge == staleSlot)slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// If we didn't find stale entry on backward scan, the// first stale entry seen while scanning for key is the// first still present in the run.if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slottab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge themif (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}/*** Heuristically scan some cells looking for stale entries.* This is invoked when either a new element is added, or* another stale one has been expunged. It performs a* logarithmic number of scans, as a balance between no* scanning (fast but retains garbage) and a number of scans* proportional to number of elements, that would find all* garbage but would cause some insertions to take O(n) time.** @param i a position known NOT to hold a stale entry. The* scan starts at the element after i.** @param n scan control: {@code log2(n)} cells are scanned,* unless a stale entry is found, in which case* {@code log2(table.length)-1} additional cells are scanned.* When called from insertions, this parameter is the number* of elements, but when from replaceStaleEntry, it is the* table length. (Note: all this could be changed to be either* more or less aggressive by weighting n instead of just* using straight log n. But this version is simple, fast, and* seems to work well.)** @return true if any stale entries have been removed.*/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 hysteresisif (size >= threshold - threshold / 4)resize();}/*** Double the capacity of the table.*/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;}/*** Expunge all stale entries in the table.*/private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);}}

    簡單總結下上面的用法

    a、探測過程中slot都不無效,并且順利找到key所在的slot,直接替換即可
    b、探測過程中發現有無效slot,調用replaceStaleEntry,效果是最終一定會把key和value放在這個slot,并且會盡可能清理無效slot
    在replaceStaleEntry過程中,如果找到了key,則做一個swap把它放到那個無效slot中,value置為新值
    在replaceStaleEntry過程中,沒有找到key,直接在無效slot原地放entry
    c、探測沒有發現key,則在連續段末尾的后一個空位置放上entry,這也是線性探測法的一部分。放完后,做一次啟發式清理,如果沒清理出去key,并且當前table大小已經超過閾值了,則做一次rehash,rehash函數會調用一次全量清理slot方法也expungeStaleEntries,如果完了之后table大小超過了threshold - threshold / 4,則進行擴容2倍
    6、ThreadLocal中的remove方法調用TreadLocalMap中的remove

    /*** Remove the entry for key.*/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;}}}

    這個方法比較簡單,找到key就清除

  • ThreadLocal會不會遇到內存泄漏問題
    有關于內存泄露是因為在有線程復用如線程池的場景中,一個線程的生命周期很長,大對象長期不被回收影響系統運行效率與安全。如果線程不會復用,用完即銷毀了也不會有ThreadLocal引發內存泄露的問題。
    仔細讀過ThreadLocalMap的源碼,可以推斷,如果在使用的ThreadLocal的過程中,習慣加上remove,這樣是不會引起內存泄漏。
    如果沒有進行remove呢?如果對應線程之后調用ThreadLocal的get和set方法都有很高的概率會順便清理掉無效對象,斷開value強引用,從而大對象被收集器回收。
    我們應該考慮到何時調用ThreadLocal的remove方法。一個比較熟悉的場景就是對于一個請求一個線程的server如tomcat,在代碼中對web api作一個切面,存放一些如用戶名等用戶信息,在連接點方法結束后,再顯式調用remove。
  • 以上都是理論,我們做一個小實驗

    /*** @author shuliangzhao* @Title: ThreadLocalDemo* @ProjectName design-parent* @Description: TODO* @date 2019/6/1 0:00*/ public class ThreadLocalDemo {private static ThreadLocal<ThreadLocalDemo> t = new ThreadLocal<>();private ThreadLocalDemo() {}public static ThreadLocalDemo getInstance() {ThreadLocalDemo threadLocalDemo = ThreadLocalDemo.t.get();if (null == threadLocalDemo) {threadLocalDemo = new ThreadLocalDemo();t.set(threadLocalDemo);}return threadLocalDemo;}private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}public static void main(String[] args) {long i = (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1));System.out.println(i);int i1 = 55&(2^2-1);System.out.println(55%2^2);System.out.println(i1);} } /*** @author shuliangzhao* @Title: ThreadLocalTest* @ProjectName design-parent* @Description: TODO* @date 2019/6/1 0:03*/ public class ThreadLocalTest {public static void main(String[] args) {for(int i=0; i<2;i++){new Thread(new Runnable() {@Overridepublic void run() {Double d = Math.random()*10;ThreadLocalDemo.getInstance().setName("name "+d);new A().get();new B().get();}}).start();}}static class A{public void get(){System.out.println(ThreadLocalDemo.getInstance().getName());}}static class B{public void get(){System.out.println(ThreadLocalDemo.getInstance().getName());}}}

    運行結果

    ?

    image.png

    到這里我們就把ThreadLocal講完了,可以多看看源碼,看看大牛們是怎么設計出如此優美的代碼。

    總結

    以上是生活随笔為你收集整理的ThreadLocal原理及用法详解的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。