介紹
- 顧名思義這個類提供線程局部變量
- 每個線程(通過其get或set方法)都有自己獨立初始化的變量副本
ThreadLocal思想
在多線程環(huán)境下秘遏,不同的線程同時訪問同一個共享變量會有并發(fā)問題说庭。一種解決方法是進行同步,例如使用synchronized。另外一種比較常見的形式就是局部(local)變量(這里排除局部變量引用指向共享對象的情況)野建,這樣資源就不是被兩個線程共享,那么也不會出現(xiàn)競爭問題垦搬。
自定義類實現(xiàn)ThreadLocal的功能
一個簡單的思路是使用 Map 存儲每個變量的副本贩绕,將當(dāng)前線程的 Name 作為 key,副本變量作為 value 值:
public class Test {
/** 用于存儲每個線程對應(yīng)的數(shù)據(jù) */
public static class CustomThreadLocal{
public final Map<String,Integer> cacheValueMap=new HashMap<String, Integer>();
private int defaultValue;
public CustomThreadLocal(int value){
defaultValue=value;
}
public void set(Integer value){
cacheValueMap.put(Thread.currentThread().getName(),value);
}
public Integer get(){
String threadName=Thread.currentThread().getName();
if(cacheValueMap.containsKey(threadName)){
return cacheValueMap.get(threadName);
}
return defaultValue;
}
}
/** 數(shù)據(jù)資源類,提供進行加減操作*/
public static class Number {
private CustomThreadLocal value = new CustomThreadLocal(0);
public void increase() throws InterruptedException {
value.set(10);
Thread.sleep(10);
System.out.println("increase value: " + value.get());
}
public void decrease() throws InterruptedException {
value.set(-10);
Thread.sleep(10);
System.out.println("decrease value: " + value.get());
}
}
public static void main(String[] args) {
final Number number=new Number();
Thread increaseThread=new Thread(new Runnable() {
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"th1");
Thread decreaseThread=new Thread(new Runnable() {
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"th2");
increaseThread.start();
decreaseThread.start();
}
}
這種寫法存在的問題:
- 即便線程執(zhí)行完赠法,只要 number 變量存在麦轰,線程的副本變量依然會存在(存放在 number 的 cacheMap 中)。
- 多個線程有可能會同時操作 cacheMap砖织,需要對 cacheMap 進行同步處理
為了解決上面的問題款侵,我們換種思路,每個線程創(chuàng)建一個 Map侧纯,存放當(dāng)前線程中副本變量新锈,用 CustomThreadLocal 的實例作為 key 值,下面是一個示例:
public class Test {
/** 自定義線程,并定義map存放當(dāng)前線程中副本變量 */
public static class ManualThread extends Thread{
public final Map<Integer,Integer> cacheValueMap=new HashMap<Integer, Integer>();
}
/** 通過實例本身映射出線程的副本數(shù)據(jù)眶熬,并對其進行操作 */
public static class CustomThreadLocal{
private int defaultValue;
public CustomThreadLocal(int value){
defaultValue=value;
}
public void set(Integer value){
Integer id = this.hashCode();
Map<Integer, Integer> cacheMap = getMap();
cacheMap.put(id, value);
}
public Integer get(){
Integer id = this.hashCode();
Map<Integer, Integer> cacheMap = getMap();
if (cacheMap.containsKey(id)) {
return cacheMap.get(id);
}
return defaultValue;
}
//注意這個方法
public Map<Integer, Integer> getMap() {
ManualThread thread = (ManualThread) Thread.currentThread();
return thread.cacheValueMap;
}
}
/** 數(shù)據(jù)資源類,提供進行加減操作*/
public static class Number {
private CustomThreadLocal value = new CustomThreadLocal(0);
public void increase() throws InterruptedException {
value.set(10);
Thread.sleep(10);
System.out.println("increase value: " + value.get());
}
public void decrease() throws InterruptedException {
value.set(-10);
Thread.sleep(10);
System.out.println("decrease value: " + value.get());
}
}
public static void main(String[] args) {
final Number number=new Number();
//使用自定義線程類
Thread increaseThread=new ManualThread(){
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread decreaseThread=new ManualThread(){
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
increaseThread.start();
decreaseThread.start();
}
}
這種寫法妹笆,當(dāng)線程消亡之后块请,線程中存放的副本變量也會被全部回收,并且 cacheMap 是線程私有的拳缠,不會出現(xiàn)多個線程并發(fā)問題负乡。在 Java 中,ThreadLocal 類的實現(xiàn)就是采用的這種思想脊凰,注意只是思想,實際的實現(xiàn)和上面的并不一樣茂腥。
基本原理
ThreadLocal 的實現(xiàn)思想狸涌,我們在前面已經(jīng)說了,每個線程維護一個 ThreadLocalMap 的映射表最岗,映射表的 key 是 ThreadLocal 實例本身帕胆,value 是要存儲的副本變量。ThreadLocal 實例本身并不存儲值般渡,它只是提供一個在當(dāng)前線程中找到副本值的 key懒豹。 如下圖所示:
API總覽
get函數(shù)用來獲取與當(dāng)前線程關(guān)聯(lián)的ThreadLocal的值,如果當(dāng)前線程沒有該ThreadLocal的值驯用,則調(diào)用initialValue函數(shù)獲取初始值返回脸秽,initialValue是protected類型的,所以一般我們使用時需要繼承該函數(shù)蝴乔,給出初始值记餐。而set函數(shù)是用來設(shè)置當(dāng)前線程的該ThreadLocal的值,remove函數(shù)用來刪除ThreadLocal綁定的值薇正,在某些情況下需要手動調(diào)用片酝,防止內(nèi)存泄露
關(guān)鍵點分析
ThreadLocal 散列值
當(dāng)創(chuàng)建了一個 ThreadLocal 的實例后,它的散列值就已經(jīng)確定了挖腰,下面是 ThreadLocal 中的實現(xiàn)
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);
}
我們看到 threadLocalHashCode 是一個常量雕沿,它通過 nextHashCode() 函數(shù)產(chǎn)生。nextHashCode() 函數(shù)其實就是在一個 AtomicInteger 變量(初始值為0)的基礎(chǔ)上每次累加 0x61c88647猴仑,使用 AtomicInteger 為了保證每次的加法是原子操作审轮。而 0x61c88647 這個就比較神奇了,它可以使 hashcode 均勻的分布在大小為 2 的 N 次方的數(shù)組里宁脊。
具體散列測試請看http://www.reibang.com/p/fe9ffcf51f4b
ThreadLocalMap
被定義為一個靜態(tài)類断国,包含的主要成員:
- 首先是Entry的定義;
- 初始的容量為INITIAL_CAPACITY = 16榆苞;
- 主要數(shù)據(jù)結(jié)構(gòu)就是一個Entry的數(shù)組table稳衬;
- size用于記錄Map中實際存在的entry個數(shù);
- threshold是擴容上限坐漏,當(dāng)size到達threashold時薄疚,需要resize整個Map碧信,threshold的初始值為len * 2 / 3;
- nextIndex和prevIndex則是為了安全的移動索引
Entry
- ThreadLocalMap 使用 Entry 類來存儲數(shù)據(jù)
- entry的key是ThreadLocal實例街夭,value是Object(即我們所謂的“線程本地數(shù)據(jù)”)
- 為避免占用空間較大或生命周期較長的數(shù)據(jù)常駐于內(nèi)存引發(fā)一系列問題砰碴,hash table的key是弱引用WeakReferences
static class Entry extends WeakReference <ThreadLocal <?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal <?> k, Object v) {
super(k);
value = v;
}
}
set 函數(shù)
private void set(ThreadLocal <?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根據(jù) ThreadLocal 的散列值,查找對應(yīng)元素在數(shù)組中的位置
int i = key.threadLocalHashCode & (len - 1);
// 使用線性探測法查找元素
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal <?> k = e.get();
// ThreadLocal 對應(yīng)的 key 存在板丽,直接覆蓋之前的值
if (k == key) {
e.value = value;
return;
}
// key為 null呈枉,但是值不為 null,說明之前的 ThreadLocal 對象已經(jīng)被回收了埃碱,當(dāng)前數(shù)組中的 Entry 是一個陳舊(stale)的元素
if (k == null) {
// 用新元素替換陳舊的元素猖辫,這個方法進行了不少的垃圾清理動作,防止內(nèi)存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal 對應(yīng)的 key 不存在并且沒有找到陳舊的元素砚殿,則在空元素的位置創(chuàng)建一個新的 Entry啃憎。
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlot 清理陳舊的 Entry(key == null),具體的參考源碼似炎。如果沒有清理陳舊的 Entry 并且數(shù)組中的元素大于了閾值辛萍,則進行 rehash。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set函數(shù)注意點
- int i = key.threadLocalHashCode & (len - 1);羡藐,這里實際上是對 len-1 進行了取余操作贩毕。之所以能這樣取余是因為 len 的值比較特殊,是 2 的 n 次方仆嗦,減 1 之后低位變?yōu)槿?1耳幢,高位變?yōu)槿?0。例如 16欧啤,減 1 之后對應(yīng)的二進制為: 00001111睛藻,這樣其他數(shù)字中大于 16 的部分就會被 0 與掉,小于 16 的部分就會保留下來邢隧,就相當(dāng)于取余了店印。
- 在 replaceStaleEntry 和 cleanSomeSlots 方法中都會清理一些陳舊的 Entry,防止內(nèi)存泄漏
threshold 的值大小為 threshold = len * 2 / 3;- rehash 方法中首先會清理陳舊的 Entry倒慧,如果清理完之后元素數(shù)量仍然大于 threshold 的 3/4按摘,則進行擴容操作(數(shù)組大小變?yōu)樵瓉淼?2倍)
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
getEntry函數(shù)
private Entry getEntry(ThreadLocal <?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
因為 ThreadLocalMap 中采用開放定址法,所以當(dāng)前 key 的散列值和元素在數(shù)組中的索引并不一定完全對應(yīng)纫谅。所以在 get 的時候炫贤,首先會看 key 的散列值對應(yīng)的數(shù)組元素是否為要查找的元素,如果不是付秕,再調(diào)用getEntryAfterMiss方法查找后面的元素
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);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
所以首先e如果為null的話兰珍,那么getEntryAfterMiss
還是直接返回null的,如果是不滿足e.get() == key
询吴,那么進入while循環(huán)掠河,這里是不斷循環(huán)亮元,如果e一直不為空,那么就調(diào)用nextIndex唠摹,不斷遞增i爆捞,在此過程中一直會做兩個判斷:
- 如果
k==key
,那么代表找到了這個所需要的Entry,直接返回勾拉; - 如果
k==null
煮甥,那么證明這個Entry中key已經(jīng)為null,那么這個Entry就是一個過期對象,這里調(diào)用expungeStaleEntry
清理該Entry藕赞。 這里就解答了導(dǎo)致內(nèi)存泄露的原因苛秕,即ThreadLocal Ref銷毀時,ThreadLocal實例由于只有Entry中的一條弱引用指著找默,那么就會被GC掉,Entry的key沒了吼驶,value可能會內(nèi)存泄露的惩激,其實在每一個get,set操作時都會不斷清理掉這種key為null的Entry的蟹演。
為什么循環(huán)查找
主要是因為處理哈希沖突的方法风钻,我們都知道HashMap采用拉鏈法處理哈希沖突,即在一個位置已經(jīng)有元素了酒请,就采用鏈表把沖突的元素鏈接在該元素后面骡技,而ThreadLocal采用的是開放地址法,即有沖突后羞反,把要插入的元素放在要插入的位置后面為null的地方布朦,具體關(guān)于這兩種方法的區(qū)別可以參考:解決哈希(HASH)沖突的主要方法。所以上面的循環(huán)就是因為我們在第一次計算出來的i位置不一定存在key與我們想查找的key恰好相等的Entry昼窗,所以只能不斷在后面循環(huán)是趴,來查找是不是被插到后面了,直到找到為null的元素澄惊,因為若是插入也是到null為止的唆途。
分析完循環(huán)的原因,其實也可以深入expungeStaleEntry看看是怎么清理的掸驱。
expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry 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;
}
看上面這段代碼主要有兩部分:
- expunge entry at staleSlot:這段主要是將i位置上的Entry的value設(shè)為null肛搬,Entry的引用也設(shè)為null,那么系統(tǒng)GC的時候自然會清理掉這塊內(nèi)存毕贼;
- Rehash until we encounter null: 這段就是掃描位置staleSlot之后温赔,null之前的Entry數(shù)組,清除每一個key為null的Entry鬼癣,同時若是key不為空让腹,做rehash远剩,調(diào)整其位置。
為什么要做rehash呢
因為我們在清理的過程中會把某個值設(shè)為null骇窍,那么這個值后面的區(qū)域如果之前是連著前面的瓜晤,那么下次循環(huán)查找時,就會只查到null為止腹纳。
舉個例子就是:...,<key1(hash1), value1>, <key2(hash1), value2>,...(即key1和key2的hash值相同) 此時痢掠,若插入<key3(hash2), value3>,其hash計算的目標位置被<key2(hash1), value2>占了嘲恍,于是往后尋找可用位置足画,hash表可能變?yōu)椋?..., <key1(hash1), value1>, <key2(hash1), value2>, <key3(hash2), value3>, ... 此時,若<key2(hash1), value2>被清理佃牛,顯然<key3(hash2), value3>應(yīng)該往前移(即通過rehash調(diào)整位置)淹辞,否則若以key3查找hash表,將會找不到key3
remove方法
刪除其實就是將 Entry 的鍵值設(shè)為 null俘侠,變?yōu)殛惻f的 Entry象缀。然后調(diào)用 expungeStaleEntry 清理陳舊的 Entry
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;
}
}
}
副本變量存取
ThreadLocal的set、get方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
return setInitialValue();
}
存取的基本流程就是首先獲得當(dāng)前線程的 ThreadLocalMap爷速,將 ThreadLocal 實例作為鍵值傳入 Map央星,然后就是進行相關(guān)的變量存取工作了。線程中的 ThreadLocalMap 是懶加載的惫东,只有真正的要存變量時才會調(diào)用 createMap 創(chuàng)建莉给,下面是 createMap 的實現(xiàn):
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
如果想要給 ThreadLocal 的副本變量設(shè)置初始值,需要重寫 initialValue 方法廉沮,如下面的形式:
ThreadLocal <Integer> threadLocal = new ThreadLocal() {
protected Integer initialValue() {
return 0;
}
};
SuppliedThreadLocal
SuppliedThreadLocal是JDK8新增的內(nèi)部類颓遏,只是擴展了ThreadLocal的初始化值的方法而已,允許使用JDK8新增的Lambda表達式賦值滞时。需要注意的是州泊,函數(shù)式接口Supplier不允許為null
static final class SuppliedThreadLocal<T>extends ThreadLocal<T>{
private final Supplier<?extends T> supplier;
SuppliedThreadLocal(Supplier<?extends T> supplier){
this.supplier= Objects.requireNonNull(supplier);
}
@Override
protected T initialValue(){
return supplier.get();
}
}
總結(jié)
為什么會內(nèi)存泄漏
ThreadLocalMap
使用ThreadLocal
的弱引用作為key
,如果一個ThreadLocal
沒有外部強引用來引用它漂洋,那么系統(tǒng) GC 的時候遥皂,這個ThreadLocal
勢必會被回收,這樣一來刽漂,ThreadLocalMap
中就會出現(xiàn)key
為null
的Entry
演训,就沒有辦法訪問這些key
為null
的Entry
的value
,如果當(dāng)前線程再遲遲不結(jié)束的話贝咙,這些key
為null
的Entry
的value
就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收样悟,造成內(nèi)存泄漏。
怎么避免內(nèi)存泄漏
每次使用完ThreadLocal,都調(diào)用它的remove()方法窟她,清除數(shù)據(jù)
為什么沒有next
ThreadLocalMap 中使用開放地址法來處理散列沖突陈症,而 HashMap 中使用的分離鏈表法。之所以采用不同的方式主要是因為:在 ThreadLocalMap 中的散列值分散的十分均勻震糖,很少會出現(xiàn)沖突录肯。并且 ThreadLocalMap 經(jīng)常需要清除無用的對象,使用純數(shù)組更加方便吊说。所以不需要next
為什么使用弱引用
因為如果這里使用普通的key-value形式來定義存儲結(jié)構(gòu)论咏,實質(zhì)上就會造成節(jié)點的生命周期與線程強綁定,只要線程沒有銷毀颁井,那么節(jié)點在GC分析中一直處于可達狀態(tài)厅贪,沒辦法被回收,而程序本身也無法判斷是否可以清理節(jié)點雅宾。弱引用是Java中四檔引用的第三檔养涮,比軟引用更加弱一些,如果一個對象沒有強引用鏈可達眉抬,那么一般活不過下一次GC贯吓。當(dāng)某個ThreadLocal已經(jīng)沒有強引用可達,則隨著它被垃圾回收吐辙,在ThreadLocalMap里對應(yīng)的Entry的鍵值會失效,這為ThreadLocalMap本身的垃圾清理提供了便利蘸劈。
如何清理entry
調(diào)用expungeStaleEntry
進行實現(xiàn)
使用場景及方式
- 主要應(yīng)用場景為按線程多實例(每個線程對應(yīng)一個實例)的對象的訪問昏苏,并且這個對象很多地方都要用到,例如數(shù)據(jù)庫連接威沫、Session管理贤惯、日志的uniqueID等
使用注意事項
- ThreadLocal實例通常來說都是private static類型
- ThreadLocal并未解決多線程訪問共享對象的問題,而是為每個線程創(chuàng)建一個單獨的變量副本棒掠,提供了保持對象的方法和避免參數(shù)傳遞的復(fù)雜性孵构;
- ThreadLocal并不是每個線程拷貝一個對象,而是直接new(新建)一個烟很;
- 如果ThreadLocal.set()的對象是多線程共享的颈墅,那么還是涉及并發(fā)問題。
- 過度使用ThreadLocal很容易加大類之間的耦合度與依賴關(guān)系(開發(fā)過程可能會不得不過度考慮某個ThreadLocal在調(diào)用時是否已有值雾袱,存放的是哪個類放的什么值)
- 應(yīng)用一定要自己負責(zé) remove恤筛,并且不要和線程池配合,因為woker線程往往是不會退出的芹橡。
參考地址
http://blog.zhangjikai.com/2017/03/29/%E3%80%90Java-%E5%B9%B6%E5%8F%91%E3%80%91%E8%AF%A6%E8%A7%A3-ThreadLocal
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
https://juejin.im/post/5a5efb1b518825732b19dca4#heading-9
https://www.cnblogs.com/micrari/p/6790229.html