java多线程:ThreadLocal详解
場景: 登錄用戶的信息保存與獲取問題。
在常規的系統設計中,后端系統通常會有一個很長的調用鏈路(Controller->Service->Dao)。
通常用戶在登陸之后,用戶信息會保存在session或token中。但假如我們在controller、service及service的多個調用方法中都要用到用戶信息相關,我們可以將User對象作為參數進行方法傳遞,也就是將User作為context上下文。但這樣極其繁瑣,對于調用鏈路長的情況也不夠優雅簡潔;同時若調用鏈涉及到第三方庫,重寫的方法無法修改參數的情況下,對象就傳遞不進去了。我們也不能直接將User對象保存為static,因為在多個用戶訪問的情況的會有并發的問題。這時候我們就可以用上ThreadLocal對象,進行全局存儲用戶信息。
提出問題:
- ThreadLocal是什么?用來解決什么問題?
- ThreadLocal的使用
- ThreadLocal的底層實現
- ThreadLocal的內存泄漏問題
ThreadLocal是什么
ThreadLocal是一個保存線程局部變量的工具,每一個線程都可以獨立地通過ThreadLocal保存與獲取自己的變量,而該變量不會受其它線程的影響。
ThreadLocal的使用
ThreadLocal主要對外提供三個方法:get(), set(T)和remove()。
通常我們將threadLocal對象設置為static,以便在全局都可獲取。
set(T):線程填充只屬于自己線程的數據,其他線程無法獲取。
get():線程獲取自己set的數據。
remove():線程移除自己設置的值。
我們模擬創建兩個用戶登錄后,保存進threadLocal中,再分別執行playGame方法。在上述方法中,我們直接根據threadLocal就正確地獲取了線程所屬的user對象,而沒有在方法上傳遞參數。
如上,我們便解決了調用鏈路過長時參數傳遞的繁瑣,免去了方法參數傳遞的過程。每個線程調用threadLocal的get方法時,獲取的都是自己set進去的值,解決了并發的問題。
ThreadLocal原理
那么,ThreadLocal是怎么實現線程局部變量的呢?
首先我們看看set方法:
看到這里答案就出來了,通過threadLocal.set(T)設置值時,實際上就是獲取當前線程的ThreadLocalMap,每個線程都持有一個ThreadLocalMap對象,該map以threadLocal為key,value即為存儲的值,保存進map中。
get():
同上,get方法實際上也是獲取當前線程持有的threadLocalMap,以當前threadLoca作為key,從map中獲取value。
總結:ThreadLocal實現線程局部變量的方法,就是每個線程都持有維護了一個threadLocalMap,在執行threadLocal對象的get和set方法時,都是獲取當前線程的map對象,再以當前的threadLocal為key,進行value的操作,從而實現了線程局部變量的隔離
ThreadLocalMap底層實現:
每個線程中都持有了一個ThreadLocalMap用來存放線程局部變量,而ThreadLocalMap是為了實現ThreadLocal功能特意編寫的map類,為什么不用現成的HashMap呢?
閱讀ThreadLocalMap的源碼,我們可以發現幾個不同的點:
1、ThreadLocalMap中Entry的key設置為了弱引用。
這是為了防止key的內存泄漏,下面再仔細講講ThreadLocal的內存泄漏問題
2、ThreadLocalMap解決hash沖突的方法。
ThreadLocalMap的hash算法為 threadLocalHashCode & (table.length - 1),而table.length指定為了2的整數次冪,因此等同于threadLocalHashCode % (table.length - 1)。
可以看到通過hash算法定位到數組下標,接著進行判斷:若該entry的k為給定的key,則直接更新value;若k為空,說明該k被垃圾回收了,entry也該執行replaceStaleEntry進行清空;若不滿足條件,則會獲取數組entry為空下一個元素,跳出for循環。因此我們可知,ThreadLocalMap解決hash沖突的方法為定位到的數組下標往后移動。
3、threadLocalHashCode為0x61c88647的整數倍。那為什么是這個魔法值呢?
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的整數冪,接下來以數組長度為16、32、64為例,探討ThreadLocal的hash值為該魔法值的整數倍,發送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));}結果分別為:
[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…,都沒有發生重復的情況。
因此我們可以得出結論:將ThreadLocal設置為該魔法值的整數倍,可以極大地減少存入ThreadLocalMap中的hash沖突的概率。 同時也不得不感慨作者的數學功底之深厚!
ThreadLocal的內存泄漏問題
剛剛我們有說到,ThreadLocalMap自定義的Entry繼承了WeakReference,實際上便是將map中的key對threadLocal進行了弱引用。
弱引用介紹:弱引用是為了解決內存泄漏問題的,若一個對象只存在弱引用,在jvm垃圾回收時便會將該對象進行回收。 場景:A a = new A();B b = new B();b.a = a;a = null; // 在這里只是將a的引用置為null,因為b.a對a還有強引用,a對象便還會存在內存中而不會被垃圾回收。 解決辦法: 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;}}引用關系如圖:將key設置為弱引用,便可在threadLocal引用被置為null時,key對threadLocal的因為都為弱引用,jvm便可對threadLocal對象進行gc,從而防止threadLocal對象的內存泄漏。
但是!可以看到value的引用為強引用,若是線程能正常結束倒也還好說,線程結束了,map、entry、value的強引用都斷開了,也就能被gc回收。但是通常情況下,因為線程的創建和銷毀比較耗費性能,我們會使用諸如線程池的方法進行線程復用,這時候線程一直不被銷毀,則很可能出現內存泄漏的問題。
解決辦法
對于value內泄露的問題,ThreadLocal的開發者也注意到了,因此在調用threadLocal的get和set方法時,在碰上key為null的情況會執行replaceStaleEntry()方法清理調entry。而對于線程復用導致的內存泄漏問題,則可以在執行完畢后調用threadLocal.remove()方法手動清理。
總結
以上是生活随笔為你收集整理的java多线程:ThreadLocal详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux开源人脸识别库,人脸识别身份验
- 下一篇: Dreambox的enigma和enig