本文詳細介紹了Java中的ThreadLocal的作用、原理、源碼以及應用,並且介紹了ThreadLocal的內存泄漏的原理以及解決辦法。
文章目錄
1 ThreadLocal的概述
1.1 ThreadLocal的入門
public class ThreadLocal< T >
extends Object
ThreadLocal來自JDK1.2,位於java.lang包。ThreadLocal可以提供線程內的局部變量,這種變量在線程的生命週期內起作用,ThreadLocal又叫做線程本地變量/線程本地存儲。
實際上,單就ThreadLocal這個類來說,它不存儲任何內容,真正存儲數據的集合在每個Thread中的threadLocals變量裏面,ThreadLocal中只是定義了這個集合的結構,並提供了一系列操作的方法。後面的源碼分析處會講到!
可以說,ThreadLocal只是一個工具類,一個對各個線程的threadLocals進行操作的工具而已。
ThreadLocal 的作用和目的:
- 用於實現線程內的數據共享,某些數據是以線程爲作用域並且不同線程具有不同的數據副本時,即數據在線程之間隔離,就可以考慮用ThreadLocal。即對於相同的程序代碼,多個模塊在同一個線程中運行時要共享一份數據,而在另外線程中運行時又共享另外一份數據。
- 方便同一個線程複雜邏輯下的數據傳遞,有些時候一個線程中的任務過於複雜,我們又需要某個數據能夠貫穿整個線程的執行過程,可能涉及到不同類/函數之間數據的傳遞。此時使用Threadlocal存放數據,在線程內部只要通過get方法就可以獲取到在該線程中存進去的數據,方便快捷。
1.2 同步和ThreadLocal
- 同步與ThreadLocal是解決多線程中數據訪問問題的兩種思路,前者是數據共享的思路,後者是數據隔離的思路。同步是一種以時間換空間的思想,ThreadLocal是一種空間換時間的思想。前者僅提供一份變量,讓不同的線程排隊訪問,實現串行化;而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
- Threadlocal並不能代替同步,注意ThreadLocal不是用來解決共享對象的多線程訪問問題的。通過ThreadLocal的set()方法設置到線程的threadLocals裏的是線程自己要存儲的對象,其他線程不需要去訪問,也是訪問不到的。各個線程中的threadLocals以及裏面的值都是不同的對象。Threadloocal是用來進行變量隔離,就是說ThreadLocal是針對那些不需要共享的屬性!
1.3 主要API方法與使用案例
ThreadLocal類主要有四個可供調用的方法:
- void set(T value):保存值;
- T get():獲取值;
- void remove():移除值;
- initialValue():返回該線程局部變量的初始值,該方法是爲了讓子類繼承而設計的。這個方法是一個延遲調用方法,在一個線程第一次調用get()時(並且set未被調用)才執行。ThreadLocal中的默認實現是直接返回一個null。
ThreadLocal實現線程內數據共享,線程間數據隔離的案例:
public class ThreadLocalTest {
/**
* 全局ThreadLocal對象位於堆中,這是線程共享的,而方法棧,是每個線程私有的
*/
static ThreadLocal<String> th = new ThreadLocal<>();
public static void set() {
//設置值,值爲當前線程的名字
th.set(Thread.currentThread().getName());
}
public static String get() {
//獲取值
return th.get();
}
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程中嘗試獲取值:" + get());
//主線程中設置值,值爲線程名字
set();
//主線程中嘗試獲取值
System.out.println("主線程中再次嘗試獲取值:" + get());
//開啓一條子線程
Thread thread = new Thread(new Th1(), "child");
thread.start();
//主線程等待子線程執行完畢
thread.join();
System.out.println("等待子線程執行完畢,主線程中再次嘗試獲取值:" + get());
}
static class Th1 implements Runnable {
@Override
public void run() {
System.out.println("子線程中嘗試獲取值:" + get());
//子線程中設置值,值爲線程名字
set();
System.out.println("子線程中再次嘗試獲取值:" + get());
}
}
}
結果如下:
主線程中嘗試獲取值:null
主線程中再次嘗試獲取值:main
子線程中嘗試獲取值:null
子線程中再次嘗試獲取值:child
等待子線程執行完畢,主線程中再次嘗試獲取值:main
先設置值,然後獲取,可以得到“main”。然後開啓子線程,在子線程內部,先獲取,得到null,然後設置值,再獲取,得到“child”。最後在主線程中再嘗試獲取,得到的還是原值“main”,這說明ThreadLocal使得變量的作用範圍限制在本線程中了,其他線程是無法訪問到該變量的。
注意這裏由於案例演示在最後並沒有調用remove方法,在實際使用中應該在使用完畢之後調用remove方法,原理後面會講!
2 ThreadLocal的原理
2.1 基本關係
ThreadLocal類中定義了一個內部類ThreadLocalMap,ThreadLocalMap是真正存放數據的容器,實際上它的底層就是一張哈希表。
每個Thread線程內部都定義有一個ThreadLocal.ThreadLocalMap類型的threadLocals變量,這樣,線程之間的ThreadLocalMap互不干擾。threadLocals變量持有的ThreadLocalMap在ThreadLocal調用set或者get方法時纔會初始化。
ThreadLocal還提供相關方法,負責向當前線程的ThreadLocalMap變量獲取和設置線程的變量值,相當於一個工具類。
當在某個線程的方法中使用ThreadLocal設置值的時候,就會將該ThreadLocal對象添加到該線程內部的ThreadLocalMap中,其中鍵就是該ThreadLocal對象,值可以是任意類型任意值。當在某個線程的方法中使用ThreadLocal獲取值的時候,會以該ThreadLocal對象爲鍵,在該線程的ThreadLocalMap中獲取對應的值。
類結構:
ThreadLocal中定義了ThreadLocalMap的結構,並提供操作的方法。
public class ThreadLocal<T> {
//……
static class ThreadLocalMap {
//……
}
/**
* ThreadLocal的構造器,裏面什麼都沒有
* 創建ThreadLocal時,沒有初始化ThreadLocalMap,在set、get方法中還可能初始化!
*/
public ThreadLocal() {
}
}
每個thread對象都持有一個ThreadLocalMap類型的引用變量,用於存放線程本地變量。key爲ThreadLocal對象,value爲要存儲的數據。
public class Thread implements Runnable {
/*與此線程相關的線程本地值。此ThreadLocalMap定義在ThreadLocal類中,使用在Thread類中*/
ThreadLocal.ThreadLocalMap threadLocals = null;
//………………
}
下面是Thread、threadlocalMap、ThreadLocal的關係圖:
2.2 基本結構
ThreadLocal中定義了ThreadLocalMap的結構。
ThreadLocalMap也是一張key-value類型的哈希表,但是ThreadLocalMap並沒有實現Map接口,它內部具有一個Entry類型的table數組用於存放節點。Entry節點用於存放key、value數據,並且繼承了WeakReference。
通過對該ThreadLocal對象進行哈希運算,可以得到該ThreadLocal對象在Entry數組中的桶位,從而找到唯一的Entry。如果發生了哈希衝突,那麼與HashMap和Hashtable採用的“鏈地址法”不同,ThreadLocalMap採用“線性探測法”解決哈希衝突,採用該方法的原因是實現很簡單,佔用更小的空間,並且一般來說一個ThreadLocalMap並不會存放很多數據!關於哈希表的原理以及各種解決哈希衝突的方法詳解,可以看這篇文章:數據結構—散列表(哈希表)的原理以及Java代碼的實現。
在創建ThreadLocalMap對象的同時即初始化16個長度的內部table數組,擴容閾值爲len * 2 / 3,擴容增量爲增加原容量的1倍。在沒有使用ThreadLocal設置、獲取值時,線程中的ThreadLocalMap對象一直爲null。
/**
* ThreadLocal的內部類ThreadLocalMap
*/
static class ThreadLocalMap {
/**
* table數組的初始化容量,
*/
private static final int INITIAL_CAPACITY = 16;
//存放數據的數組,在創建ThreadLocalMap對象時將會初始化該數組,大小必須是2^N次方
private Entry[] table;
//擴容閾值,爲len * 2 / 3
private int threshold;
/**
* 內部節點對象,貌似沒找到“key”字段在哪裏,實際上存放k的屬性位於其父類WeakReference的父類Reference中,名爲referent,即屬性複用
* 插入數據時,通過對key(threadLocal對象)的hash計算,來找出Entry應該存放的table數組的桶位,
* 不過可能造成hash衝突,它採用線性探測法解決衝突,因此需要線性向後查找。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
//存放值
Object value;
//構造器
Entry(ThreadLocal<?> k, Object v) {
//調用父類的構造器,傳入key,這裏k被包裝成爲弱引用
//實際上存放k的屬性位於其父類WeakReference的父類Reference中,名爲referent,即屬性複用
super(k);
value = v;
}
}
}
2.3 set方法
set方法是由ThreadLocal提供的,用於存放數據,大概步驟如下:
- 獲取當前線程的成員變量threadLocals;
- 如果threadLocals不等於null,則調用set方法存放數據,方法結束;
- 否則,調用createMap方法初始化threadLocals,然後存放數據,方法結束。
/**
* ThreadLocal中的方法,開放給外部調用的存放數據的方法
*
* @param value 需要存放的數據
*/
public void set(T value) {
//注意,這裏首先獲取當前線程t
Thread t = Thread.currentThread();
//1.1 然後通過getMap方法,傳入t,獲取當前t線程的threadLocals
ThreadLocalMap map = getMap(t);
//如果map存在,則存放數據
if (map != null)
//this代指當前ThreadLocal對象,value表示值
map.set(this, value);
else
//如果不存在,則構建屬於當前線程的ThreadLocalMap並存放數據
createMap(t, value);
}
/**
* 1.1 ThreadLocal中的方法,獲取指定線程的threadLocals
*
* @param t 指定線程
* @return t的threadLocals
*/
ThreadLocalMap getMap(Thread t) {
//t代表當前線程,獲取該線程的threadLocals屬性,該屬性就是一個ThreadLocalMap,默認爲null
return t.threadLocals;
}
/**
* 1.2 ThreadLocal中的方法,用於構建ThreadLocalMap對象並賦值
*
* @param t 當前線程
* @param firstValue 要存入的值
*/
void createMap(Thread t, T firstValue) {
//該方法是ThreadLocal中的方法,this代指當前ThreadLocal對象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 位於ThreadLocalMap中的構造器,用於創建新的ThreadLocalMap對象
*
* @param firstKey key
* @param firstValue value
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//創建table數組,初始容量爲INITIAL_CAPACITY,即16
table = new Entry[INITIAL_CAPACITY];
//尋找數組桶位,通過ThreadLocal對象的threadLocalHashCode屬性 & 15
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//該位置存放元素,由於是剛創建對象,因此不存在哈希衝突的情況,直接存儲就行了
//構造器在“基本結構”部分分析過,key最終被包裝成弱引用。
table[i] = new Entry(firstKey, firstValue);
//size設置爲1
size = 1;
//setThreshold方法設置擴容閥值
setThreshold(INITIAL_CAPACITY);
}
/**
* ThreadLocalMap中的方法,設置擴容閾值
*
* @param len 數組長度
*/
private void setThreshold(int len) {
//數組長度的2/3
threshold = len * 2 / 3;
}
2.3.1 內部set方法
上面的set方法中,如果當前t線程的threadLocals不爲null,那麼又調用了另一個私有的set方法存放數據。該方法是ThreadLocal的核心方法之一,並且比較複雜,大概具有如下步驟:
- 通過哈希算法計算出當前key存放的桶位i,並獲取i的元素e。
- 如果e不爲空,說明發生哈希衝突,使用線性探測法替換或者存放數據:
a) 如果e的key和指定key相等(使用==比較),那麼替換value,方法結束;
b) 否則,如果e的key等於null,那說明是無效數據。調用replaceStaleEntry從該索引開始清理無效數據,並且存放新數據,在replaceStaleEntry過程中:
i. 如果找到了key相等的entry,則它放到無效桶位中,value置爲新值,方法結束。
ii. 如果沒找到key相等的entry,直接在無效slot原地放entry,方法結束。
iii.調用到了replaceStaleEntry方法,那就肯定能將新數據存入ThreadLocalMap中,並且不再執行後續步驟。
c) 否則,nextIndex方法獲取下一個索引並賦值給i,如果該位置的節點e爲null,則結束循環,否則進行下一次循環; - 走到這一步,說明沒有替換value,也沒有沒有進行無效數據清理,而是找到了一個空桶位i,直接在該位置插入新entry,此時肯定保證最初始的i和現在的之間的位置都是存在有效節點的;
- 存放元素完畢之後,再調用cleanSomeSlots做一次部分無效節點清理,如果沒清理出去key(返回false)並且當前table大小 大於等於 閾值,則調用rehash方法;
- rehash方法中會調用一次全表掃描清理的方法即expungeStaleEntries()方法。如果expungeStaleEntries完畢之後table大小還是大於等於(threshold – threshold / 4),則調用resize方法進行擴容;
- resize方法將擴容兩倍,同時完成節點的轉移。
ThreadLocalMap使用==比較key是否相同。ThreadLocalMap解決Hash衝突的方式就是簡單的步長加1或減1(線性探測),尋找下一個相鄰的位置。當向前尋找到數組頭部或者向後尋找到數組尾部的時候,下一個位置就是數組尾部或者數組頭部,即循環查找。
/**
* 位於ThreadLocalMap內的set方法,用於存放數據。
*
* @param key ThreadLocal對象
* @param value 值
*/
private void set(ThreadLocal<?> key, Object value) {
//tab保存數組引用
Entry[] tab = table;
//len保存數組的度
int len = tab.length;
/*1 哈希算法計算桶位 通過ThreadLocal的threadLocalHashCode屬性計算出該key(ThreadLocal對象)對應的數組桶位i*/
int i = key.threadLocalHashCode & (len - 1);
/*
* 2 使用線性探測法存放元素,可能進行垃圾清理
* 獲取i索引位置的Entry e,如果e不爲null,說明發生了哈希衝突,下面開始解決:
* 判斷兩個key是否相等,即是否需要進行value替換,如果相等,則替換value,解決完畢,方法返回;
* 否則,判斷獲取的e的key是否爲null,如果獲取的ThreadLocal爲null,這說明該WeakReference(弱引用)被回收了(因爲Entry繼承了WeakReference),
* 說明ThreadLocal肯定在外部沒有強引用了,這個Entry變成了垃圾,調用replaceStaleEntry方法擦除該位置或者其他的無效的Entry,重新賦值,解決完畢,方法返回,這是爲了防止內存泄漏
* 否則判斷該位置i是否爲null,即沒有節點,如果爲null,則在該位置新建節點並插入,解決完畢。
* 否則,i = nextIndex(i, len),嘗試下一次循環。
* 如果循環完畢,方法還沒結束,那說明沒找到key相等的節點和key==null的節點,但是找到了下一個節點爲null的桶位,記錄此時索引值i,將會在該位置插入新節點。
*
* 這就是ThreadLocalMap解決哈希衝突的辦法,即開放定址法——線性探測:當衝突時,向下查找下一個節點爲null的位置存放新節點
* nextIndex()方法用於循環數組索引,即如果初始i爲15,長度爲16,那麼nextIndex將返回0,如果初始i爲1,長度爲16,那麼nextIndex將返回2。
* 這樣的做法有利於利用起始索引前面的空間
* */
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
/*獲取該Entry的key,即原來的ThreadLocal對象,這是其父類Reference的方法*/
ThreadLocal<?> k = e.get();
/*如果獲取的ThreadLocal和要存的ThreadLocal是同一個對象,那麼就替換值,方法結束
* 這裏能夠看出,判斷key相等的條件是兩個對象使用==比較返回true
* */
if (k == key) {
e.value = value;
return;
}
/*
*如果獲取的ThreadLocal爲null,這說明該WeakReference(弱引用)被回收了(因爲Entry繼承了WeakReference),
* 說明ThreadLocal肯定在外部沒有強引用了,這個Entry變成了垃圾,擦除該位置的Entry,重新賦值並結束方法,這是爲了防止內存泄漏*/
if (k == null) {
/*
* 從該位置開始,繼續尋找key,並且會盡可能清理其他無效slot
* 在replaceStaleEntry過程中,如果找到了key,則做一個swap把它放到那個無效slot中,value置爲新值
* 在replaceStaleEntry過程中,沒有找到key,直接在該無效slot原地放entry
* */
replaceStaleEntry(key, value, i);
return;
}
}
/*
* 執行到這一步方法還沒有返回,說明i位置沒有節點,此時e等於null,直接在該位置插入新的Entry
* 此時肯定保證最初始的i和現在的之間的位置是存在節點的!
* */
tab[i] = new Entry(key, value);
//size自增1,使用sz記錄
int sz = ++size;
/* 3 嘗試清理垃圾,然後判斷是否需要擴容,如果需要那就擴容
* 存放完畢元素之後,再調用cleanSomeSlots做一次垃圾清理,如果沒清理出去key(返回false)
* 並且當前table大小大於等於閾值,則調用rehash方法
* rehash方法中會調用一次全量清理slot方法也即expungeStaleEntries()方法
* 如果expungeStaleEntries完畢之後table大小還是大於等於(threshold – threshold / 4),則調用resize方法進行擴容
* resize方法將擴容兩倍,同時完成節點的轉移
* */
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 在length的索引範圍內獲取i的下一個索引,循環
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 在length的索引範圍內獲取i的上一個索引,循環
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
2.3.2 哈希算法
在set方法中,我們可以找到ThreadLocalMap的哈希算法爲:
int i = key.threadLocalHashCode & (len - 1);
由於len長度一定是2的冪次方,因此上面的位運算可以轉換爲key.threadLocalHashCode% len,所以說ThreadLocalMap的哈希算法也是一種取模(求餘)算法,因爲餘數一定會比除數小,那麼計算出來的桶位肯定是位於[0, len-1]之間了,剛好在底層數組的索引範圍內,還是比較簡單的。
這裏的key我們知道是ThreadLocal對象,這個threadLocalHashCode屬性看名字猜測就是該對象的哈希值了,那麼這個值是通過hashCode方法得到的嗎?實際上,threadLocalHashCode這個屬性的得來非常的有趣,我們必須要去ThreadLocal源碼中去看看!
public class ThreadLocal<T> {
/**
* 下一個hashCode
* 注意:這是個靜態屬性,那麼只有在ThreadLocal的類第一次被加載進行類初始化的時候會被初始化,明顯,初始化時爲0。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* threadlocal對象的hashcode,並非通過HashCode方法得到,他有自己的計算規則
* 可以看到,它是調用nextHashCode()方法的返回值得來的
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 每個threadLocal對象通過該方法獲取自己的hashcode
*/
private static int nextHashCode() {
//內部使用nextHashCode對象的getAndAdd方法
//該方法首先返回當前的值,然後使得當前值的值加上指定的值,這裏是HASH_INCREMENT
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 哈希增量,顧名思義,就是哈希值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;
}
結合上面的幾個屬性和方法,我們終於明白:
在第一次創建ThreadLocal實例時,會加載ThreadLocal類,此時nextHashCode初始化值爲0,然後是該對象threadLocalHashCode屬性的初始化,在創建該類對象完畢之後,會自動調用nextHashCode方法,將此時nextHashCode的值作爲自己的hashCode並且nextHashCode對象的值增加HASH_INCREMENT,明顯是作爲下一個ThreadLocal實例的hashCode值。
即,每一個ThreadLocal實例使用創建該實例時的nextHashCode值作爲自己的hashCode,然後將nextHashCode值增加HASH_INCREMENT,作爲下一個ThreadLocal實例的hashCode。
0x61c88647是十六進制的數,轉換爲十進制就是1640531527,實際上這個哈希增量的值的選取和斐波那契散列法、黃金比例有關(https://www.javaspecialists.eu/archive/Issue164.html),目的是爲了讓哈希碼能更加均勻的分佈在2的N次方的數組裏。
2.4 get方法
對於不同的線程,每次獲取變量值時,是從本線程內部的threadLocals中獲取的,別的線程並不能獲取到當前線程的值,形成了變量的隔離,互不干擾。大概步驟如下:
- 獲取當前線程的成員變量threadLocals
- 如果threadLocals非空,調用getEntry方法嘗試查找並返回節點e:
a) 如果e不爲null,說明找到了,那愛麼返回e的value,方法結束
b) 如果e爲null,說明沒找到,方法繼續。 - 到這一步,說明可能是threadLocals爲空,或者沒找到e。那麼調用setInitialValue方法,以當前ThreadLocal對象爲key設置一個entry,並返回value。
/**
* ThreadLocal中的get方法,開放給外部調用
*
* @return 當前線程的當前ThreadLocal對象存入的值
*/
public T get() {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取當前線程的threadLocals對象
ThreadLocalMap map = getMap(t);
//如果map不爲null,即表示已經初始化過
if (map != null) {
//從map獲取對應的Entry節點,傳入this代表當前的ThreadLocal對象
ThreadLocalMap.Entry e = map.getEntry(this);
//如果e不爲null
if (e != null) {
//獲取並返回值
T result = (T) e.value;
return result;
}
}
//否則,如果map爲null,或者e爲null
//那麼返回null或者自定義的初始值
return setInitialValue();
}
2.4.1 getEntry方法
ThreadLocalMap內部的方法,根據key,嘗試獲取對應的Entry節點。 大概步驟如下:
- 根據key計算出桶位;
- 獲取該桶位節點e,如果e不爲null並且e的key和指定key相等(使用==比較),那麼返回e,方法結束;
- 否則,調用getEntryAfterMiss方法進行一個步長的線性探測查找,查找過程中每碰到無效的節點,調用expungeStaleEntry進行清理;如果找到了則返回找到的entry;沒有找到(探測到了空的桶位),則返回null。
/**
* ThreadLocalMap內部的方法,根據key,獲取對應的Entry節點
*
* @param key key
* @return Entry節點,沒找到就返回null
*/
private Entry getEntry(ThreadLocal<?> key) {
//根據key計算桶位
int i = key.threadLocalHashCode & (table.length - 1);
//獲取Entry節點e
Entry e = table[i];
/*如果e不爲nul並且並且e內部key等於當前key(ThreadLocal對象)*/
//可以看到key相等是使用==直接比較的
if (e != null && e.get() == key)
//則返回e
return e;
else
/*否則使用線性探測查找
線性探測查找過程中每碰到無效slot,調用expungeStaleEntry進行清理;如果找到了則返回entry;沒有找到,返回null*/
return getEntryAfterMiss(key, i, e);
}
2.4.2 setInitialValue方法
ThreadLocal的方法,用於設置並返回初始值,在get方法沒有找key對應的節點時,會調用該方法! 大概有如下幾步:
- 獲取initialValue方法的返回值,作爲新節點的value;
- 獲取當前線程的ThreadLocalMap,判斷是否爲null;
- 如果不爲null,則以當前ThreadLocal對象爲key,存放value,方法結束;
- 如果爲null,則初始化此線程的ThreadLocalMap,並以當前ThreadLocal對象爲key,存放value,方法結束。
/**
* ThreadLocal的方法,設置並返回初始值
*
* @return 返回null或者通過initialValue方法用戶自定義的初始值
*/
private T setInitialValue() {
//返回null或者用戶重寫該方法時自定義的返回值
T value = initialValue();
//獲取當前線程
Thread t = Thread.currentThread();
//獲取當前線程的map
ThreadLocal.ThreadLocalMap map = getMap(t);
//如果map不爲null
if (map != null)
//嘗試添加節點,以當前ThreadLocal對象爲key,以null或者自定義的初始值爲value
map.set(this, value);
else
//否則初始化map並設置值
createMap(t, value);
//返回value,null或者自定義的初始值
return value;
}
2.4.2.1 initialValue方法
當get方法沒有找到數據時,會調用setInitialValue方法,該方法中會調用initialValue方法,將默認返回null,用戶也可以重寫該方法,用於返回指定的值,相當於默認初始值。
setInitialValue將會以當前ThreadLocal對象爲key,initialValue的返回值爲value,存放一個節點,同時返回value的值。
/**
* ThreadLocal的方法,默認返回空
*
* @return 默認返回null, 用戶可以重寫該方法返回自定義的初始值
*/
protected T initialValue() {
return null;
}
2.4.2.2 默認初始值案例
public class ThreadLocalInitialValue {
public static void main(String[] args) {
//th1覆寫了initialValue方法
ThreadLocal th1 = new ThreadLocal() {
@Override
protected Object initialValue() {
return 11;
}
};
//th2沒有覆寫了initialValue方法
ThreadLocal th2 = new ThreadLocal();
//由於並沒有調用set方法設置數據,那麼兩個ThreadLocal的get方法都將不能找到存放的數據
//此時th1將返回默認初始值,並設置key:th1 value:11
System.out.println("th1初始值:" + th1.get());
//th2將返回null,並設置key:th2 value:null
System.out.println("th2初始值:" + th2.get());
}
}
2.5 ThreadLocal的內存泄露
2.5.1 內存泄漏的原理
首先是基礎知識,關於Java中的引用的介紹:Java中強、軟、弱、虛四種對象引用的詳解和案例演示。
根據上面的源碼,我們知道在存放新節點時在Entry節點的構造器中,並不是直接使用ThreadLocal對象作爲key的,而是使用由ThreadLocal對象包裝成的弱引用對象作爲Key的。
爲什麼使用弱引用包裝的ThreadLocal對象作爲key?因爲如果某個entry直接使使用一個普通屬性和ThreadLocal對象關聯,即key時強引用。那麼當最外面ThreadLocal對象的全局變量引用置空時,由於在ThreadLocalMap中存在key對這個ThreadLocal對象的強引用,那麼這個ThreadLocal對象並不會被回收,但此時我們已經無法訪問、利用這個對象,造成了key的內存泄漏。
因此,ThreadLocal對象被包裝爲弱引用作爲key。這樣,當外部的ThreadLocal對象的強引用被清除時,由於在ThreadLocalMap中存儲的是弱引用key,那麼下一次GC時這個弱引用可以被清除,這個ThreadLocal對象也就可以被清除了。但是仍然會造成內存泄漏,不過此時是value的內存泄漏。
我們知道value是強引用。這就導致了一個問題,如果這個弱引用key被回收而變成null時,如果之前調用ThreadLocal方法設置值的線程一直持續運行,那麼它的ThreadLocalMap也一直存在,那麼內部的entry節點也一直存在,那麼value肯定還存在,但是此時卻不能通過key訪問到了(因爲key被回收變成null了),此時還是發生了內存泄露。
2.5.2 如何避免內存泄漏
我們在set和get方法的源碼中能夠看到,當遍歷的entry的key爲null時,此時將清除該entry,value置空,這樣就可以解決部分內存泄漏問題。但這並不是絕對的,可能並沒有遍歷到key爲null的entry時set、get方法就因爲插入、獲取成功而返回了,因此在set、get方法中,只會嘗試將遍歷的到無效數據清除,並且這種方式是一種被動的清除,不能即時清除無效數據。
ThreadLocal還有一個remove方法,該方法可以將此ThreadLocal對象對應的entry清除。實際上,在對ThreadLocal的數據使用完畢之後,從邏輯上來說此時的entry就是無效的數據了,因此主動調用一次remove方法,將該entry移除。這樣我們對使用完畢的entry進行手動清除,從根本上杜絕了內存泄漏問題。
所以養成良好的編程習慣十分重要,使用完ThreadLocal的數據之後,一定要記得調用一次remove方法。
3 總結與應用
總結
每個ThreadLocal由於實現線程本地存儲,但是隻能保存一個本地數據,如果想要一個線程能夠保存多個數據,就需要創建多個ThreadLocal。
ThreadLocalMap的key鍵爲ThreadLocal包裝成的弱引用,會有內存泄漏的風險,因此使用完畢必須手動調用remove清除!
應用
使用ThreadLocal的典型場景正如上面的數據庫連接管理,線程會話管理等場景,只適用於獨立變量副本的情況,如果變量爲全局共享的,則不適用在高併發下使用。
spring對於有狀態的單例Bean,比如RequestContextHolder。將它們在多線程下可能發生線程安全問題的屬性使用ThreadLocal封裝,這樣每條線程還是訪問同一份對象,但是使用的對象中的數據卻是線程私有的。
原始的JDBC方式的時候可以使用ThreadLocal類來管理事務!
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!