這4種ThreadLocal你都知道嗎?
什么是ThreadLocal
ThreadLocal類顧名思義可以理解為線程本地變量。也就是說(shuō)如果定義了一個(gè)ThreadLocal, 每個(gè)線程往這個(gè)ThreadLocal中讀寫是線程隔離,互相之間不會(huì)影響的。它提供了一種將可變數(shù)據(jù)通過(guò)每個(gè)線程有自己的獨(dú)立副本從而實(shí)現(xiàn)線程封閉的機(jī)制。
實(shí)際應(yīng)用
實(shí)際開(kāi)發(fā)中我們真正使用ThreadLocal的場(chǎng)景還是比較少的,大多數(shù)使用都是在框架里面。最常見(jiàn)的使用場(chǎng)景的話就是用它來(lái)解決數(shù)據(jù)庫(kù)連接、Session管理等保證每一個(gè)線程中使用的數(shù)據(jù)庫(kù)連接是同一個(gè)。還有一個(gè)用的比較多的場(chǎng)景就是用來(lái)解決SimpleDateFormat解決線程不安全的問(wèn)題,不過(guò)現(xiàn)在java8提供了DateTimeFormatter它是線程安全的,感興趣的同學(xué)可以去看看。還可以利用它進(jìn)行優(yōu)雅的傳遞參數(shù),傳遞參數(shù)的時(shí)候,如果父線程生成的變量或者參數(shù)直接通過(guò)ThreadLocal傳遞到子線程參數(shù)就會(huì)丟失,這個(gè)后面會(huì)介紹一個(gè)其他的ThreadLocal來(lái)專門解決這個(gè)問(wèn)題的。
ThreadLocal api介紹
ThreadLocal的API還是比較少的就幾個(gè)api
我們看下這幾個(gè)api的使用,使用起來(lái)也超級(jí)簡(jiǎn)單
- private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"java金融");
 - public static void main(String[] args) {
 - System.out.println("獲取初始值:"+threadLocal.get());
 - threadLocal.set("關(guān)注:【java金融】");
 - System.out.println("獲取修改后的值:"+threadLocal.get());
 - threadLocal.remove();
 - }
 
輸出結(jié)果:
- 獲取初始值:java金融
 - 獲取修改后的值:關(guān)注:【java金融】
 
是不是炒雞簡(jiǎn)單,就幾行代碼就把所有api都覆蓋了。下面我們就來(lái)簡(jiǎn)單看看這幾個(gè)api的源碼吧。
成員變量
- /**初始容量,必須為2的冪
 - * The initial capacity -- MUST be a power of two.
 - */
 - private static final int INITIAL_CAPACITY = 16;
 - /** Entry表,大小必須為2的冪
 - * The table, resized as necessary.
 - * table.length MUST always be a power of two.
 - */
 - private Entry[] table;
 - /**
 - * The number of entries in the table.
 - */
 - private int size = 0;
 - /**
 - * The next size value at which to resize.
 - */
 - private int threshold; // Default to 0
 
這里會(huì)有一個(gè)面試經(jīng)常問(wèn)到的問(wèn)題:為什么entry數(shù)組的大小,以及初始容量都必須是2的冪?對(duì)于 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 以及很多源碼里面都是使用 hashCode &( -1) 來(lái)代替hashCode% 。這種寫法好處如下:
- 使用位運(yùn)算替代取模,提升計(jì)算效率。
 - 為了使不同 hash 值發(fā)生碰撞的概率更小,盡可能促使元素在哈希表中均勻地散列。
 
set方法
- public void set(T value) {
 - Thread t = Thread.currentThread();
 - ThreadLocalMap map = getMap(t);
 - if (map != null)
 - map.set(this, value);
 - else
 - createMap(t, value);
 - }
 
set方法還是比較簡(jiǎn)單的,我們可以重點(diǎn)看下這個(gè)方法里面的ThreadLocalMap,它既然是個(gè)map(注意不要與java.util.map混為一談,這里指的是概念上的map),肯定是有自己的key和value組成,我們根據(jù)源碼可以看出它的key是其實(shí)可以把它簡(jiǎn)單看成是ThreadLocal,但是實(shí)際上ThreadLocal中存放的是ThreadLocal的弱引用,而它的value的話是我們實(shí)際set的值
- static class Entry extends WeakReference<ThreadLocal<?>> {
 - /** The value associated with this ThreadLocal. */
 - Object value; // 實(shí)際存放的值
 - Entry(ThreadLocal<?> k, Object v) {
 - super(k);
 - value = v;
 - }
 - }
 
Entry就是是ThreadLocalMap里定義的節(jié)點(diǎn),它繼承了WeakReference類,定義了一個(gè)類型為Object的value,用于存放塞到ThreadLocal里的值。我們?cè)賮?lái)看下這個(gè)ThreadLocalMap是位于哪里的?我們看到ThreadLocalMap 是位于Thread里面的一個(gè)變量,而我們的值又是放在ThreadLocalMap,這樣的話我們就實(shí)現(xiàn)了每個(gè)線程間的隔離。下面兩張圖的基本就把ThreadLocal的結(jié)構(gòu)給介紹清楚了。
接下來(lái)我們?cè)倏聪耇hreadLocalMap里面的數(shù)據(jù)結(jié)構(gòu),我們知道HaseMap解決hash沖突是由鏈表和紅黑樹(shù)(jdk1.8)來(lái)解決的,但是這個(gè)我們看到ThreadLocalMap只有一個(gè)數(shù)組,它是怎么來(lái)解決hash沖突呢?ThreadLocalMap采用「線性探測(cè)」的方式,什么是線性探測(cè)呢?就是根「據(jù)初始key的hashcode值確定元素在table數(shù)組中的位置,如果發(fā)現(xiàn)這個(gè)位置上已經(jīng)有其他key值的元素被占用,則利用固定的算法尋找一定步長(zhǎng)的下個(gè)位置,依次判斷,直至找到能夠存放的位置」。ThreadLocalMap解決Hash沖突的方式就是簡(jiǎn)單的步長(zhǎng)加1或減1,尋找下一個(gè)相鄰的位置。
- /**
 - * Increment i modulo len.
 - */
 - private static int nextIndex(int i, int len) {
 - return ((i + 1 < len) ? i + 1 : 0);
 - }
 - /**
 - * Decrement i modulo len.
 - */
 - private static int prevIndex(int i, int len) {
 - return ((i - 1 >= 0) ? i - 1 : len - 1);
 - }
 
這種方式的話如果一個(gè)線程里面有大量的ThreadLocal就會(huì)產(chǎn)生性能問(wèn)題,因?yàn)槊看味夹枰獙?duì)這個(gè)table進(jìn)行遍歷,清空無(wú)效的值。所以我們?cè)谑褂玫臅r(shí)候盡可能的使用少的ThreadLocal,不要在線程里面創(chuàng)建大量的ThreadLocal,如果需要設(shè)置不同的參數(shù)類型我們可以通過(guò)ThreadLocal來(lái)存放一個(gè)Object的Map這樣的話,可以大大減少創(chuàng)建ThreadLocal的數(shù)量。偽代碼如下:
- public final class HttpContext {
 - private HttpContext() {
 - }
 - private static final ThreadLocal<Map<String, Object>> CONTEXT = ThreadLocal.withInitial(() -> new ConcurrentHashMap(64));
 - public static <T> void add(String key, T value) {
 - if(StringUtils.isEmpty(key) || Objects.isNull(value)) {
 - throw new IllegalArgumentException("key or value is null");
 - }
 - CONTEXT.get().put(key, value);
 - }
 - public static <T> T get(String key) {
 - return (T) get().get(key);
 - }
 - public static Map<String, Object> get() {
 - return CONTEXT.get();
 - }
 - public static void remove() {
 - CONTEXT.remove();
 - }
 - }
 
這樣的話我們?nèi)绻枰獋鬟f不同的參數(shù),可以直接使用一個(gè)ThreadLocal就可以代替多個(gè)ThreadLocal了。如果覺(jué)得不想這么玩,我就是要?jiǎng)?chuàng)建多個(gè)ThreadLocal,我的需求就是這樣,而且性能還得要好,這個(gè)能不能實(shí)現(xiàn)列?可以使用netty的FastThreadLocal可以解決這個(gè)問(wèn)題,不過(guò)要配合使FastThreadLocalThread或者它子類的線程線程效率才會(huì)更高,更多關(guān)于它的使用可以自行查閱資料哦。
下面我們先來(lái)看下它的這個(gè)哈希函數(shù)
- // 生成hash code間隙為這個(gè)魔數(shù),可以讓生成出來(lái)的值或者說(shuō)ThreadLocal的ID較為均勻地分布在2的冪大小的數(shù)組中。
 - private static final int HASH_INCREMENT = 0x61c88647;
 - /**
 - * Returns the next hash code.
 - */
 - private static int nextHashCode() {
 - return nextHashCode.getAndAdd(HASH_INCREMENT);
 - }
 
可以看出,它是在上一個(gè)被構(gòu)造出的ThreadLocal的ID/threadLocalHashCode的基礎(chǔ)上加上一個(gè)魔數(shù)0x61c88647的。這個(gè)魔數(shù)的選取與斐波那契散列有關(guān),0x61c88647對(duì)應(yīng)的十進(jìn)制為1640531527.當(dāng)我們使用0x61c88647這個(gè)魔數(shù)累加對(duì)每個(gè)ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪(數(shù)組的長(zhǎng)度)取模,得到的結(jié)果分布很均勻。我們可以來(lái)也演示下通過(guò)這個(gè)魔數(shù)
- public class MagicHashCode {
 - private static final int HASH_INCREMENT = 0x61c88647;
 - public static void main(String[] args) {
 - hashCode(16); //初始化16
 - hashCode(32); //后續(xù)2倍擴(kuò)容
 - hashCode(64);
 - }
 - private static void hashCode(Integer length) {
 - int hashCode = 0;
 - for (int i = 0; i < length; i++) {
 - hashCode = i * HASH_INCREMENT + HASH_INCREMENT;//每次遞增HASH_INCREMENT
 - System.out.print(hashCode & (length - 1));
 - System.out.print(" ");
 - }
 - System.out.println();
 - }
 - }
 
運(yùn)行結(jié)果:
- 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
 - 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0
 
不得不佩服下這個(gè)作者,通過(guò)使用了斐波那契散列法,來(lái)保證哈希表的離散度,讓結(jié)果很均勻。可見(jiàn)「代碼要寫的好,數(shù)學(xué)還是少不了」啊。其他的源碼就不分析了,大家感興趣可以自行去查看下。
ThreadLocal的內(nèi)存泄露
關(guān)于ThreadLocal是否會(huì)引起內(nèi)存泄漏也是一個(gè)比較有爭(zhēng)議性的問(wèn)題。首先我們需要知道什么是內(nèi)存泄露?
❝在Java中,內(nèi)存泄漏就是存在一些被分配的對(duì)象,這些對(duì)象有下面兩個(gè)特點(diǎn),首先,這些對(duì)象是可達(dá)的,即在有向圖中,存在通路可以與其相連;其次,這些對(duì)象是無(wú)用的,即程序以后不會(huì)再使用這些對(duì)象。如果對(duì)象滿足這兩個(gè)條件,這些對(duì)象就可以判定為Java中的內(nèi)存泄漏,這些對(duì)象不會(huì)被GC所回收,然而它卻占用內(nèi)存。❞
ThreadLocal的內(nèi)存泄露情況:
- 線程的生命周期很長(zhǎng),當(dāng)ThreadLocal沒(méi)有被外部強(qiáng)引用的時(shí)候就會(huì)被GC回收(給ThreadLocal置空了):ThreadLocalMap會(huì)出現(xiàn)一個(gè)key為null的Entry,但這個(gè)Entry的value將永遠(yuǎn)沒(méi)辦法被訪問(wèn)到(后續(xù)在也無(wú)法操作set、get等方法了)。如果當(dāng)這個(gè)線程一直沒(méi)有結(jié)束,那這個(gè)key為null的Entry因?yàn)橐泊嬖趶?qiáng)引用(Entry.value),而Entry被當(dāng)前線程的ThreadLocalMap強(qiáng)引用(Entry[] table),導(dǎo)致這個(gè)Entry.value永遠(yuǎn)無(wú)法被GC,造成內(nèi)存泄漏。下面我們來(lái)演示下這個(gè)場(chǎng)景
 
- public static void main(String[] args) throws InterruptedException {
 - ThreadLocal<Long []> threadLocal = new ThreadLocal<>();
 - for (int i = 0; i < 50; i++) {
 - run(threadLocal);
 - }
 - Thread.sleep(50000);
 - // 去除強(qiáng)引用
 - threadLocal = null;
 - System.gc();
 - System.runFinalization();
 - System.gc();
 - }
 - private static void run(ThreadLocal<Long []> threadLocal) {
 - new Thread(() -> {
 - threadLocal.set(new Long[1024 * 1024 *10]);
 - try {
 - Thread.sleep(1000000000);
 - } catch (InterruptedException e) {
 - e.printStackTrace();
 - }
 - }).start();
 - }
 
通過(guò)jdk自帶的工具jconsole.exe會(huì)發(fā)現(xiàn)即使執(zhí)行了gc 內(nèi)存也不會(huì)減少,因?yàn)閗ey還被線程強(qiáng)引用著。效果圖如下:
- 針對(duì)于這種情況ThreadLocalMap在設(shè)計(jì)中,已經(jīng)考慮到這種情況的發(fā)生,你只要調(diào)用了set()、get()、remove()方法都會(huì)調(diào)用cleanSomeSlots()、expungeStaleEntry()方法去清除key為null的value。這是一種被動(dòng)的清理方式,但是如果ThreadLocal的set(),get(),remove()方法沒(méi)有被調(diào)用,就會(huì)導(dǎo)致value的內(nèi)存泄漏。它的文檔推薦我們使用static修飾的ThreadLocal,導(dǎo)致ThreadLocal的生命周期和持有它的類一樣長(zhǎng),由于ThreadLocal有強(qiáng)引用在,意味著這個(gè)ThreadLocal不會(huì)被GC。在這種情況下,我們?nèi)绻皇謩?dòng)刪除,Entry的key永遠(yuǎn)不為null,弱引用也就失去了意義。所以我們?cè)谑褂玫臅r(shí)候盡可能養(yǎng)成一個(gè)好的習(xí)慣,使用完成后手動(dòng)調(diào)用下remove方法。其實(shí)實(shí)際生產(chǎn)環(huán)境中我們手動(dòng)remove大多數(shù)情況并不是為了避免這種key為null的情況,更多的時(shí)候,是為了保證業(yè)務(wù)以及程序的正確性。比如我們下單請(qǐng)求后通過(guò)ThreadLocal構(gòu)建了訂單的上下文請(qǐng)求信息,然后通過(guò)線程池異步去更新用戶積分,這時(shí)候如果更新完成,沒(méi)有進(jìn)行remove操作,即使下一次新的訂單會(huì)覆蓋原來(lái)的值但是也是有可能會(huì)導(dǎo)致業(yè)務(wù)問(wèn)題。如果不想手動(dòng)清理是否還有其他方式解決下列?FastThreadLocal 可以去了解下,它提供了自動(dòng)回收機(jī)制。
 
在線程池的場(chǎng)景,程序不停止,線程一直在復(fù)用的話,基本不會(huì)銷毀,其實(shí)本質(zhì)就跟上面例子是一樣的。如果線程不復(fù)用,用完就銷毀了就不會(huì)存在泄露的情況。因?yàn)榫€程結(jié)束的時(shí)候會(huì)jvm主動(dòng)調(diào)用exit方法清理。
- /**
 - * This method is called by the system to give a Thread
 - * a chance to clean up before it actually exits.
 - */
 - private void exit() {
 - if (group != null) {
 - group.threadTerminated(this);
 - group = null;
 - }
 - /* Aggressively null out all reference fields: see bug 4006245 */
 - target = null;
 - /* Speed the release of some of these resources */
 - threadLocals = null;
 - inheritableThreadLocals = null;
 - inheritedAccessControlContext = null;
 - blocker = null;
 - uncaughtExceptionHandler = null;
 - }
 
InheritableThreadLocal
文章開(kāi)頭有提到過(guò)父子之間線程的變量傳遞丟失的情況。但是InheritableThreadLocal提供了一種父子線程之間的數(shù)據(jù)共享機(jī)制。可以解決這個(gè)問(wèn)題。
- static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 - static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
 - public static void main(String[] args) throws InterruptedException {
 - threadLocal.set("threadLocal主線程的值");
 - Thread.sleep(100);
 - new Thread(() -> System.out.println("子線程獲取threadLocal的主線程值:" + threadLocal.get())).start();
 - Thread.sleep(100);
 - inheritableThreadLocal.set("inheritableThreadLocal主線程的值");
 - new Thread(() -> System.out.println("子線程獲取inheritableThreadLocal的主線程值:" + inheritableThreadLocal.get())).start();
 - }
 
輸出結(jié)果
- 線程獲取threadLocal的主線程值:null
 - 子線程獲取inheritableThreadLocal的主線程值:inheritableThreadLocal主線程的值
 
但是InheritableThreadLocal和線程池使用的時(shí)候就會(huì)存在問(wèn)題,因?yàn)樽泳€程只有在線程對(duì)象創(chuàng)建的時(shí)候才會(huì)把父線程inheritableThreadLocals中的數(shù)據(jù)復(fù)制到自己的inheritableThreadLocals中。這樣就實(shí)現(xiàn)了父線程和子線程的上下文傳遞。但是線程池的話,線程會(huì)復(fù)用,所以會(huì)存在問(wèn)題。如果要解決這個(gè)問(wèn)題可以有什么辦法列?大家可以思考下,或者在下方留言哦。如果實(shí)在不想思考的話,可以參考下阿里巴巴的transmittable-thread-local哦。
總結(jié)
- 大概介紹了ThreadLocal的常見(jiàn)用法,以及大致實(shí)現(xiàn)原理,以及關(guān)于ThreadLocal的內(nèi)存泄露問(wèn)題,以及關(guān)于使用它需要注意的事項(xiàng),以及如何解決父子線程之間的傳遞。介紹了ThreadLocal、InheritableThreadLocal、FastThreadLocal、transmittable-thread-local各種使用場(chǎng)景,以及需要注意的事項(xiàng)。本文重點(diǎn)介紹了ThreadLocal,如果把這個(gè)弄清楚了,其他幾種ThreadLocal就更好理解了。
 
本文轉(zhuǎn)載自微信公眾號(hào)「java金融」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系java金融公眾號(hào)。




















 
 
 













 
 
 
 