java多线程:ThreadLocal详解
場景: 登錄用戶的信息保存與獲取問題。
在常規(guī)的系統(tǒng)設(shè)計中,后端系統(tǒng)通常會有一個很長的調(diào)用鏈路(Controller->Service->Dao)。
通常用戶在登陸之后,用戶信息會保存在session或token中。但假如我們在controller、service及service的多個調(diào)用方法中都要用到用戶信息相關(guān),我們可以將User對象作為參數(shù)進(jìn)行方法傳遞,也就是將User作為context上下文。但這樣極其繁瑣,對于調(diào)用鏈路長的情況也不夠優(yōu)雅簡潔;同時若調(diào)用鏈涉及到第三方庫,重寫的方法無法修改參數(shù)的情況下,對象就傳遞不進(jìn)去了。我們也不能直接將User對象保存為static,因?yàn)樵诙鄠€用戶訪問的情況的會有并發(fā)的問題。這時候我們就可以用上ThreadLocal對象,進(jìn)行全局存儲用戶信息。
提出問題:
- ThreadLocal是什么?用來解決什么問題?
- ThreadLocal的使用
- ThreadLocal的底層實(shí)現(xiàn)
- ThreadLocal的內(nèi)存泄漏問題
ThreadLocal是什么
ThreadLocal是一個保存線程局部變量的工具,每一個線程都可以獨(dú)立地通過ThreadLocal保存與獲取自己的變量,而該變量不會受其它線程的影響。
ThreadLocal的使用
ThreadLocal主要對外提供三個方法:get(), set(T)和remove()。
通常我們將threadLocal對象設(shè)置為static,以便在全局都可獲取。
set(T):線程填充只屬于自己線程的數(shù)據(jù),其他線程無法獲取。
get():線程獲取自己set的數(shù)據(jù)。
remove():線程移除自己設(shè)置的值。
我們模擬創(chuàng)建兩個用戶登錄后,保存進(jìn)threadLocal中,再分別執(zhí)行playGame方法。在上述方法中,我們直接根據(jù)threadLocal就正確地獲取了線程所屬的user對象,而沒有在方法上傳遞參數(shù)。
如上,我們便解決了調(diào)用鏈路過長時參數(shù)傳遞的繁瑣,免去了方法參數(shù)傳遞的過程。每個線程調(diào)用threadLocal的get方法時,獲取的都是自己set進(jìn)去的值,解決了并發(fā)的問題。
ThreadLocal原理
那么,ThreadLocal是怎么實(shí)現(xiàn)線程局部變量的呢?
首先我們看看set方法:
看到這里答案就出來了,通過threadLocal.set(T)設(shè)置值時,實(shí)際上就是獲取當(dāng)前線程的ThreadLocalMap,每個線程都持有一個ThreadLocalMap對象,該map以threadLocal為key,value即為存儲的值,保存進(jìn)map中。
get():
同上,get方法實(shí)際上也是獲取當(dāng)前線程持有的threadLocalMap,以當(dāng)前threadLoca作為key,從map中獲取value。
總結(jié):ThreadLocal實(shí)現(xiàn)線程局部變量的方法,就是每個線程都持有維護(hù)了一個threadLocalMap,在執(zhí)行threadLocal對象的get和set方法時,都是獲取當(dāng)前線程的map對象,再以當(dāng)前的threadLocal為key,進(jìn)行value的操作,從而實(shí)現(xiàn)了線程局部變量的隔離
ThreadLocalMap底層實(shí)現(xiàn):
每個線程中都持有了一個ThreadLocalMap用來存放線程局部變量,而ThreadLocalMap是為了實(shí)現(xiàn)ThreadLocal功能特意編寫的map類,為什么不用現(xiàn)成的HashMap呢?
閱讀ThreadLocalMap的源碼,我們可以發(fā)現(xiàn)幾個不同的點(diǎn):
1、ThreadLocalMap中Entry的key設(shè)置為了弱引用。
這是為了防止key的內(nèi)存泄漏,下面再仔細(xì)講講ThreadLocal的內(nèi)存泄漏問題
2、ThreadLocalMap解決hash沖突的方法。
ThreadLocalMap的hash算法為 threadLocalHashCode & (table.length - 1),而table.length指定為了2的整數(shù)次冪,因此等同于threadLocalHashCode % (table.length - 1)。
可以看到通過hash算法定位到數(shù)組下標(biāo),接著進(jìn)行判斷:若該entry的k為給定的key,則直接更新value;若k為空,說明該k被垃圾回收了,entry也該執(zhí)行replaceStaleEntry進(jìn)行清空;若不滿足條件,則會獲取數(shù)組entry為空下一個元素,跳出for循環(huán)。因此我們可知,ThreadLocalMap解決hash沖突的方法為定位到的數(shù)組下標(biāo)往后移動。
3、threadLocalHashCode為0x61c88647的整數(shù)倍。那為什么是這個魔法值呢?
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);}我們已知table.length為2的整數(shù)冪,接下來以數(shù)組長度為16、32、64為例,探討ThreadLocal的hash值為該魔法值的整數(shù)倍,發(fā)送hash沖突的情況:
public static void main(String[] args){hash(16);hash(32);hash(64);}public static void hash(int length){final int HASH_INCREMENT = 0x61c88647;int[] table = new int[length];int hash = 0;for (int i = 0; i <length ; i++) {hash += HASH_INCREMENT;table[i] = hash & (length-1);}Arrays.sort(table);System.out.println(Arrays.toString(table));}結(jié)果分別為:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
同時將長度拓展到64、128、256…,都沒有發(fā)生重復(fù)的情況。
因此我們可以得出結(jié)論:將ThreadLocal設(shè)置為該魔法值的整數(shù)倍,可以極大地減少存入ThreadLocalMap中的hash沖突的概率。 同時也不得不感慨作者的數(shù)學(xué)功底之深厚!
ThreadLocal的內(nèi)存泄漏問題
剛剛我們有說到,ThreadLocalMap自定義的Entry繼承了WeakReference,實(shí)際上便是將map中的key對threadLocal進(jìn)行了弱引用。
弱引用介紹:弱引用是為了解決內(nèi)存泄漏問題的,若一個對象只存在弱引用,在jvm垃圾回收時便會將該對象進(jìn)行回收。 場景:A a = new A();B b = new B();b.a = a;a = null; // 在這里只是將a的引用置為null,因?yàn)閎.a對a還有強(qiáng)引用,a對象便還會存在內(nèi)存中而不會被垃圾回收。 解決辦法: WeakReference<A> wr = new WeakReference<>(a);b.wr = wr; //將b對a的引用改為弱引用 static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}引用關(guān)系如圖:將key設(shè)置為弱引用,便可在threadLocal引用被置為null時,key對threadLocal的因?yàn)槎紴槿跻?#xff0c;jvm便可對threadLocal對象進(jìn)行g(shù)c,從而防止threadLocal對象的內(nèi)存泄漏。
但是!可以看到value的引用為強(qiáng)引用,若是線程能正常結(jié)束倒也還好說,線程結(jié)束了,map、entry、value的強(qiáng)引用都斷開了,也就能被gc回收。但是通常情況下,因?yàn)榫€程的創(chuàng)建和銷毀比較耗費(fèi)性能,我們會使用諸如線程池的方法進(jìn)行線程復(fù)用,這時候線程一直不被銷毀,則很可能出現(xiàn)內(nèi)存泄漏的問題。
解決辦法
對于value內(nèi)泄露的問題,ThreadLocal的開發(fā)者也注意到了,因此在調(diào)用threadLocal的get和set方法時,在碰上key為null的情況會執(zhí)行replaceStaleEntry()方法清理調(diào)entry。而對于線程復(fù)用導(dǎo)致的內(nèi)存泄漏問題,則可以在執(zhí)行完畢后調(diào)用threadLocal.remove()方法手動清理。
總結(jié)
以上是生活随笔為你收集整理的java多线程:ThreadLocal详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux开源人脸识别库,人脸识别身份验
- 下一篇: Dreambox的enigma和enig