Java多線程中ThreadLocal解讀
一、ThreadLocal簡介
1、介紹
多線程訪問同一個共享變量的時候容易出現(xiàn)并發(fā)問題,因此為了保證線程安全性,我們都會采用加鎖的方式。
而ThreadLocal是除加鎖方式之外的另一種保證線程安全性的方法。
ThreadLocal通過每個線程保存一份資源的副本,且這個副本只能被當前線程訪問。這樣的話每個線程都擁有一份,那么自然而然也就不需要對資源競爭了。
在JDK8之前,每個ThreadLocal都創(chuàng)建一個ThreadLocalMap,用線程作為ThreadLocalMap的key,要存儲的局部變量作為ThreadLocalMap的value,這樣就實現(xiàn)了各個線程的局部變量的隔離作用。

而在JDK8之后,每個線程維護一個ThreadLocalMap,這個ThreadLocalMap的key是ThreadLocal實例本身,value才是真正要存儲的變量副本。線程內(nèi)部的Map由ThreadLocal維護,由ThreadLocal負責向map獲取和設(shè)置線程的變量值。對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾。

好處:
- 減少ThreadLocalMap存儲的Entry數(shù)量:因為之前的存儲數(shù)量由Thread的數(shù)量決定,現(xiàn)在是由ThreadLocal的數(shù)量決定。在實際運用當中,往往ThreadLocal的數(shù)量要少于Thread的數(shù)量
- 當Thread銷毀之后,對應(yīng)的ThreadLocalMap也會隨之銷毀,能減少內(nèi)存的使用(但是不能避免內(nèi)存泄漏問題,解決內(nèi)存泄漏問題應(yīng)該在使用完后及時調(diào)用remove()對ThreadMap里的Entry對象進行移除,由于Entry繼承了弱引用類,會在下次GC時被JVM回收)
2、常用方法
在ThreadLocal類中,有以下幾個比較常用的方法:
- get:用于獲取 ThreadLocal 在當前線程中保存的變量副本。
- set:用于設(shè)置當前線程中變量的副本。
- remove:用于刪除當前線程中變量的副本。如果此線程局部變量隨后被當前線程讀取,則其值將通過調(diào)用其 initialValue 方法重新初始化,除非其值由中間線程中的當前線程設(shè)置。 這可能會導致當前線程中多次調(diào)用 initialValue 方法。
- initialValue:為 ThreadLocal 設(shè)置默認的 get 初始值,需要重寫 initialValue 方法 。
3、案例
public class TestThreadLocal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
TestThreadLocal testThreadLocal = new TestThreadLocal();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
testThreadLocal.setName(Thread.currentThread().getName() + "的信息");
System.out.println(Thread.currentThread().getName() + ": " + testThreadLocal.getName());
}
}).start();
}
}
}執(zhí)行結(jié)果:

從結(jié)果可以看出多個線程在訪問同一個變量的時候出現(xiàn)的異常,線程間的數(shù)據(jù)沒有隔離。下面我們來看下采用 ThreadLocal 的方式來解決這個問題的例子。
public class TestThreadLocal {
private static ThreadLocal<String> t = new ThreadLocal<>();
private String name;
public String getName() {
return t.get();
}
public void setName(String name) {
t.set(name);
}
public static void main(String[] args) {
TestThreadLocal testThreadLocal = new TestThreadLocal();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
testThreadLocal.setName(Thread.currentThread().getName() + "的信息");
System.out.println(Thread.currentThread().getName() + ": " + testThreadLocal.getName());
}
}).start();
}
}
}執(zhí)行結(jié)果:

二、ThreadLocal原理分析
1、ThreadLocal的存儲結(jié)構(gòu)
在Thread類中維護著ThreadLocal.ThreadLocalMap類型的成員threadLocals,這個成員用來存儲當前線程獨占的變量副本。ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類,它維護者一個Entry數(shù)組,用來存儲鍵值對。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}可以看到:Entry的key是ThreadLocal對象,value則是傳遞進行的對象,即變量副本,且Entry繼承了WeakReference。
2、set(T value)方法源碼
源碼如下:
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取threalLocals
ThreadLocalMap map = getMap(t);
//將ThreadLocal和值一起存入當前線程的ThreadLocalMap中
if (map != null)
//如果存在就調(diào)用map.set(),這里的this指的是調(diào)用此方法的ThreadLocal對象
map.set(this, value);
else
createMap(t, value);
}
//獲取當前線程維護的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//創(chuàng)建當前線程
void createMap(Thread t, T firstValue) {
//這里的this是調(diào)用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}在ThreadLocal的set方法中,首先獲取當前線程,然后調(diào)用getMap(Thread t)獲取當前線程中ThreaLocalMap類型的threadLocals字段。然后將ThreadLocal一塊放入到當前線程的ThreadLocalMap中。如果獲取到的Map不為空,則將參數(shù)設(shè)置到Map中,否則就創(chuàng)建Map,并設(shè)置初值。
3、get()方法
源碼如下:
public T get() {
//獲取當前線程
Thread t = Thread.currentThread();
//拿到threadLocals字段
ThreadLocalMap map = getMap(t);
//如果map存在
if (map != null) {
//獲取對應(yīng)的存儲實體
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//獲取存儲尸體對應(yīng)的value值
T result = (T)e.value;
return result;
}
}
//執(zhí)行當前代碼的兩種情況:1、map不存在,說明此線程沒有ThreadLocalMap對象;2、map存在,但是沒有與當前ThreadLocal關(guān)聯(lián)的Entry
return setInitialValue();
}
private T setInitialValue() {
// 調(diào)用initialValue獲取初始化的值
// 此方法可以被子類重寫, 如果不重寫默認返回null
T value = initialValue();
// 獲取當前線程對象
Thread t = Thread.currentThread();
// 獲取此線程對象中維護的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
// 判斷map是否存在
if (map != null)
// 存在則調(diào)用map.set設(shè)置此實體entry
map.set(this, value);
else
// 1)當前線程Thread 不存在ThreadLocalMap對象
// 2)則調(diào)用createMap進行ThreadLocalMap對象的初始化
// 3)并將 t(當前線程)和value(t對應(yīng)的值)作為第一個entry存放至ThreadLocalMap中
createMap(t, value);
// 返回設(shè)置的值value
return value;
}執(zhí)行流程:先獲取到當前線程,并根據(jù)當前線程獲取一個Map。如果獲取的map不為空,則在Map中以ThreadLocal的引用作為key在map中獲取對應(yīng)的Entry。如果entry不為空則返回e.value;如果e為空或者Map為空,則通過initialValue函數(shù)獲取初始值value,然后用ThreadLocal的引用和value作為firstKey和firstValue創(chuàng)建一個新的Map。
4、remove()方法
源碼如下:
public void remove() {
// 獲取當前線程對象中維護的ThreadLocalMap對象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在則調(diào)用map.remove
// 以當前ThreadLocal為key刪除對應(yīng)的實體entry
m.remove(this);
}執(zhí)行流程:首先獲取當前線程,并根據(jù)當前線程獲取一個Map。如果獲取的map不為空,則移除當前ThreadLocal對象對應(yīng)的Entry。
5、initialValue()方法
源碼如下:
/**
* 返回當前線程對應(yīng)的ThreadLocal的初始值
* 此方法的第一次調(diào)用發(fā)生在,當線程通過get方法訪問此線程的ThreadLocal值時
* 除非線程先調(diào)用了set方法,在這種情況下,initialValue 才不會被這個線程調(diào)用。
* 通常情況下,每個線程最多調(diào)用一次這個方法。
*
* <p>這個方法僅僅簡單的返回null {@code null};
* 如果想ThreadLocal線程局部變量有一個除null以外的初始值,
* 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
* 通常, 可以通過匿名內(nèi)部類的方式實現(xiàn)
*
* @return 當前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}該方法作用是返回該線程局部變量的初始值。從上面的代碼我們得知,在set方法還未調(diào)用而先調(diào)用了get方法時才執(zhí)行,并且僅執(zhí)行1次,這個方法缺省實現(xiàn)直接返回一個null。如果想要一個除null之外的初始值,可以重寫此方法。(備注: 該方法是一個protected的方法,顯然是為了讓子類覆蓋而設(shè)計的)
6、如何解決hash沖突問題
從前面可以看到ThreadLocalMap是類似于Map的數(shù)據(jù)結(jié)構(gòu),但是它并沒有實現(xiàn)Map接口,那么它也就不支持Map接口中的next方法。因此可以推斷出:ThreadLocalMap并不是通過拉鏈法來解決hash沖突問題的。實際上,ThreadLocalMap是使用線性探測的方式來解決hash沖突的,即:根據(jù)初始key的hashcode值來確定元素在table數(shù)組中的位置,如果這個位置已經(jīng)被其他的key占據(jù),那么就通過算法尋找下一個位置,直到找到能夠存放的位置為止。
在前面查看set()方法源碼的時候,當map為空時調(diào)用了cerateMap()方法,而該方法中調(diào)用了有參構(gòu)造:ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
/*
* firstKey : 本ThreadLocal實例(this)
* firstValue : 要保存的線程本地變量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//計算索引(重點代碼)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//設(shè)置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//設(shè)置閾值
setThreshold(INITIAL_CAPACITY);
}構(gòu)造函數(shù)首先創(chuàng)建一個長度為16的Entry數(shù)組,然后計算出firstKey對應(yīng)的索引,然后存儲到table中,并設(shè)置size和threshold。
重點看一下計算索引這一塊:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//AtomicInteger是一個提供原子操作的Integer類,通過線程安全的方式操作加減,適合高并發(fā)情況下的使用
private static AtomicInteger nextHashCode = new AtomicInteger();
//特殊的hash值
private static final int HASH_INCREMENT = 0x61c88647;這里定義了一個AtomicInteger類型,每次獲取當前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,這個值跟斐波那契數(shù)列(黃金分割數(shù))有關(guān),其主要目的就是為了讓哈希碼能均勻的分布在2的n次方的數(shù)組里, 也就是Entry[] table中,這樣做可以盡量避免hash沖突。
關(guān)于& (INITIAL_CAPACITY - 1)
計算hash的時候里面采用了hashCode & (size - 1)的算法,這相當于取模運算hashCode % size的一個更高效的實現(xiàn)。正是因為這種算法,我們要求size必須是2的整次冪,這也能保證在索引不越界的前提下,使得hash發(fā)生沖突的次數(shù)減小。
來看下ThreadLocalMap中的set方法:
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//計算索引(重點代碼,剛才分析過了)
int i = key.threadLocalHashCode & (len-1);
/**
* 使用線性探測法查找元素(重點代碼)
*/
for (ThreadLocal.ThreadLocalMap.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)被回收了,
// 當前數(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;
/**
* cleanSomeSlots用于清除那些e.get()==null的元素,
* 這種數(shù)據(jù)key關(guān)聯(lián)的對象已經(jīng)被回收,所以這個Entry(table[index])可以被置null。
* 如果沒有清除任何entry,并且當前使用量達到了負載因子所定義(長度的2/3),那么進行 * rehash(執(zhí)行一次全表的掃描清理工作)
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 獲取環(huán)形數(shù)組的下一個索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}綜上所述,代碼執(zhí)行流程如下:
- 首先根據(jù)key計算出索引 i,然后查找i位置上的Entry
- 若是Entry已經(jīng)存在并且key等于傳入的key,那么這時候直接給這個Entry賦新的value值
- 若是Entry存在,但是key為null,則調(diào)用replaceStaleEntry來更換這個key為空的Entry
- 不斷循環(huán)檢測,直到遇到為null的地方,這時候要是還沒在循環(huán)過程中return,那么就在這個null的位置新建一個Entry,并且插入,同時size增加1
- 最后調(diào)用cleanSomeSlots,清理key為null的Entry,最后返回是否清理了Entry,接下來再判斷sz 是否>= thresgold達到了rehash的條件,達到的話就會調(diào)用rehash函數(shù)執(zhí)行一次全表的掃描清理
三、ThreadLocalMap的內(nèi)存泄漏問題
在前面我們看到了,ThreadLocalMap的Entry繼承了WeakReference,其中key是弱引用,而value是強引用。如果ThreadLocal對象沒有外部強引用來引用它,那么ThreadLocal對象會在下次GC時候被回收。此時,如果Entry中的key已經(jīng)被回收,但是value又是強引用,并不會被垃圾收集器回收。如果創(chuàng)建ThreadLocal的線程一直持續(xù)運行,那么value就會一直得不到回收,從而產(chǎn)生內(nèi)存泄漏。
在Java中有四種引用類型,這里介紹一下強引用和弱引用:
- ? 強引用(“Strong” Reference),就是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活著”,垃圾回收器就不會回收這種對象。?
- 弱引用(WeakReference),垃圾回收器一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內(nèi)存空間足夠與否,都會回收它的內(nèi)存。
1、如果key使用強引用
如下圖:

假設(shè)在業(yè)務(wù)代碼中使用完ThreadLocal ,threadLocal Ref被回收了。但是因為threadLocalMap的Entry強引用了threadLocal,造成threadLocal無法被回收。
在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下,始終有強引用鏈 threadRef->currentThread->threadLocalMap->entry,Entry就不會被回收(Entry中包括了ThreadLocal實例和value),導致Entry內(nèi)存泄漏。
也就是說,ThreadLocalMap中的key使用了強引用, 是無法完全避免內(nèi)存泄漏的。
2、如果key使用了弱引用
如下圖:

假設(shè)在業(yè)務(wù)代碼中使用完ThreadLocal ,threadLocal Ref被回收了。由于ThreadLocalMap只持有ThreadLocal的弱引用,沒有任何強引用指向threadlocal實例, 所以threadlocal就可以順利被gc回收,此時Entry中的key=null。但是在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下,也存在有強引用鏈 threadRef->currentThread->threadLocalMap->entry -> value ,value不會被回收, 而這塊value永遠不會被訪問到了,導致value內(nèi)存泄漏。也就是說,ThreadLocalMap中的key使用了弱引用, 也有可能內(nèi)存泄漏。
既然弱引用和強引用都無法阻止內(nèi)存泄漏的問題,那么為啥還要選擇弱引用?
根據(jù)剛才的分析, 我們知道了: 無論ThreadLocalMap中的key使用哪種類型引用都無法完全避免內(nèi)存泄漏,跟使用弱引用沒有關(guān)系。
? 要避免內(nèi)存泄漏有兩種方式:
- 使用完ThreadLocal,調(diào)用其remove方法刪除對應(yīng)的Entry
- 使用完ThreadLocal,當前Thread也隨之運行結(jié)束
相對第一種方式,第二種方式顯然更不好控制,特別是使用線程池的時候,線程結(jié)束是不會銷毀的。也就是說,只要記得在使用完ThreadLocal及時的調(diào)用remove,無論key是強引用還是弱引用都不會有問題。那么為什么key要用弱引用呢?
? 事實上,在ThreadLocalMap中的set/getEntry方法中,會對key為null(也即是ThreadLocal為null)進行判斷,如果為null的話,那么是會對value置為null的。這就意味著使用完ThreadLocal,CurrentThread依然運行的前提下,就算忘記調(diào)用remove方法,弱引用比強引用可以多一層保障:弱引用的ThreadLocal會被回收,對應(yīng)的value在下一次ThreadLocalMap調(diào)用set,get,remove中的任一方法的時候會被清除,從而避免內(nèi)存泄漏。
解決辦法:使用ThreadLocal的set方法,顯式調(diào)用remove方法。
ThreadLocal<String> threadLocal = new ThreadLocal();
try {
threadLocal.set("xxx");
// ...
} finally {
threadLocal.remove();
}四、總結(jié)
- threadLocal操作的是每個線程自己的ThreadLocalMap,因此不會有線程的競爭。key是當前的threadlocal對象,value是當前線程放在這個threadlocal里面的值。
- ThreadLocalMap是基于開放定址法(線性探測再散列)實現(xiàn)的hash表,這里沒有采用HashMap的數(shù)組加鏈表的實現(xiàn)方式是因為這里的場景決定了hash表中不會有太多的值,通過采用獨特的斐波那契散列求hash值,可以極大的降低hash沖突概率,訪問數(shù)據(jù)速度也比HashMap快。
- ThreadLocalMap的實現(xiàn)類似WeakHashMap實現(xiàn),都通過WeakReference封裝了key值,防止內(nèi)存泄漏;
- ThreadLocalMap實現(xiàn)的挺復(fù)雜的,主要是為了避免內(nèi)存泄露,加了好多處理過期數(shù)據(jù)的操作,這就在線程中添加了多余的操作。所以如果確定ThreadLocal沒有用的話,可以調(diào)用ThreadLocal的remove()方法,這樣就避免了在ThreadLocalMap中查找過期數(shù)據(jù)并處理的操作。
到此這篇關(guān)于Java多線程中ThreadLocal解讀的文章就介紹到這了,更多相關(guān)ThreadLocal解讀內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java 根據(jù)經(jīng)緯度獲取地址實現(xiàn)代碼
這篇文章主要介紹了 java 根據(jù)經(jīng)緯度獲取地址實現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-05-05
Java8 LocalDateTime極簡時間日期操作小結(jié)
這篇文章主要介紹了Java8-LocalDateTime極簡時間日期操作整理,通過實例代碼給大家介紹了java8 LocalDateTime 格式化問題,需要的朋友可以參考下2020-04-04

