對(duì)ThreadLocal的實(shí)現(xiàn)一直比較模棱兩可,于是簡(jiǎn)單總結(jié)一下。若有錯(cuò)誤,歡迎指出,共同進(jìn)步~,如果您覺得有用,煩請(qǐng)點(diǎn)個(gè)贊哈。
個(gè)人認(rèn)為博客原文地址排版更有利于閱讀:圖說(shuō)ThreadLocal
眾所周知,ThreadLocal的設(shè)計(jì)原理是給每個(gè)線程保存一份獨(dú)立的變量的副本。這樣就可以避免因?yàn)槎鄠€(gè)線程對(duì)同一份資源(變量)的操作而產(chǎn)生一些并發(fā)問(wèn)題。相比于同步鎖來(lái)說(shuō),是空間換時(shí)間的做法。在許多框架中,應(yīng)用非常廣泛。比如Spring的Singleton作用域模式,以及Struts2的ActionContext等等。
簡(jiǎn)單使用樣例
要理解一樣?xùn)|西,首先要學(xué)會(huì)使用,還是老的思路,先寫個(gè)基本的使用的栗子。
這個(gè)栗子中,ThreadLocalDemo 擁有一個(gè)ThreadLocal類型的靜態(tài)變量local,然后在main方法中啟動(dòng)了兩個(gè)線程,這兩個(gè)線程分別對(duì)local中的數(shù)據(jù)(一個(gè)Integer類型的變量)進(jìn)行初始化,然后進(jìn)行加的操作,在操作的過(guò)程中使用了Thread.sleep(100L)增大兩個(gè)線程交叉執(zhí)行的幾率。
public class ThreadLocalDemo {
private static ThreadLocal<Integer> local = new ThreadLocal<Integer>();
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
local.set(0);
Integer n = local.get();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程r1:" + n++);
local.set(n);
}
}
};
Runnable r2 = new Runnable() {
@Override
public void run() {
local.set(100);
Integer n = local.get();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程r2:" + n++);
local.set(n);
}
}
};
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
運(yùn)行結(jié)果:
線程r1:0
線程r2:100
線程r1:1
線程r2:101
線程r1:2
線程r2:102
線程r1:3
線程r2:103
線程r1:4
線程r2:104
線程r1:5
線程r2:105
線程r1:6
線程r2:106
線程r1:7
線程r1:8
線程r2:107
線程r1:9
線程r2:108
線程r2:109
從結(jié)果看到,兩個(gè)線程的操作互不干擾,最后的輸出結(jié)果與各自線程單獨(dú)執(zhí)行的結(jié)果一致。這就是因?yàn)閮蓚€(gè)線程各自保存了這個(gè)變量的副本。此處如果在main方法中設(shè)置初始值(local.set(0))而不在各自的線程里設(shè)置,那么在r1和r2中取值時(shí)會(huì)報(bào)空指針異常,進(jìn)一步說(shuō)明了這個(gè)問(wèn)題:ThreadLocal中的變量是存在當(dāng)前線程中的。
本例中是將一個(gè)Integer存入到ThreadLocal中,如果我們將這個(gè)Integer換成一個(gè)map,那么我們就可以在一個(gè)ThreadLocal存放很多我們自己定義的數(shù)據(jù)了。
原理解析
宏觀關(guān)系分析
先上個(gè)圖就一目了然了,根據(jù)這個(gè)圖做一個(gè)簡(jiǎn)單的分析。

圖中左邊是ThreadLocal類,右邊就是Thread類,也就是線程類。從宏觀上來(lái)說(shuō):
ThreadLocal中擁有一個(gè)內(nèi)部類ThreadLocalMap、一個(gè)set方法、一個(gè)get方法。
ThreadLocalMap是中是一個(gè)Entry結(jié)構(gòu)(鍵值對(duì),可以類比HashMap),這個(gè)Entry的key是一個(gè)ThreadLocal對(duì)象。
Thread類中聲明了一個(gè)ThreadLocalMap類型的變量。
ThreadLocal中的set方法、get方法實(shí)際上是在操作Thread類中的threadLocals。
源碼分析
ThreadLocal的set、get方法
set方法:
public void set(T value) {
//獲取當(dāng)前線程的ThreadLocalMap,如果不為空
//以當(dāng)前ThreadLocal對(duì)象為key,將當(dāng)前value存進(jìn)去
//如果為空,就創(chuàng)建一個(gè)新的map
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
//創(chuàng)建Entry數(shù)組,計(jì)算hashcode值
//如果hashcode對(duì)應(yīng)的位置已經(jīng)有Entry值,那么查看下一個(gè)位置,直到找到空的位置的下標(biāo)
//這里有一個(gè)細(xì)節(jié),如果找到一個(gè)位置有Entry值,但是其Entry的key為null(說(shuō)明其ThreadLocal對(duì)象已經(jīng)被回收,后面會(huì)講到出現(xiàn)這種狀況的原因)
//這個(gè)時(shí)候會(huì)調(diào)用replaceStaleEntry進(jìn)行一個(gè)替換的操作,將當(dāng)前key/value存入進(jìn)去
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法:
public T get() {
//獲取當(dāng)前線程的ThreadLocalMap
//如果不為空,以當(dāng)前ThreadLocal對(duì)象為key,獲取value值
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;
}
}
//如果map為null,設(shè)置初始值
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap
在set方法的時(shí)候,有一個(gè)檢查ThreadLocalMap中鍵值對(duì)的key為null,但是本身Entry對(duì)象不為null的細(xì)節(jié),為了明白這個(gè)細(xì)節(jié),最后我們看ThreadLocalMap的部分源碼:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
……
}
這里ThreadLocalMap中的Entry繼承了WeakReference,WeakReference是用來(lái)做什么的呢?
下面這段話摘自《深入理解Java虛擬機(jī)》:
無(wú)論是通過(guò)引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量, 還是通過(guò)可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá), 判定對(duì)象是否存活都與“引用”有關(guān)。 在JDK 1.2以前, Java中的引用的定義很傳統(tǒng): 如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址, 就稱這塊內(nèi)存代表著一個(gè)引用。 這種定義很純粹, 但是太過(guò)狹隘, 一個(gè)對(duì)象在這種定義下只有被引用或者沒有被引用兩種狀態(tài), 對(duì)于如何描述一些“食之無(wú)味, 棄之可惜”的對(duì)象就顯得無(wú)能為力。 我們希望能描述這樣一類對(duì)象: 當(dāng)內(nèi)存空間還足夠時(shí), 則能保留在內(nèi)存之中; 如果內(nèi)存空間在進(jìn)行垃圾收集后還是非常緊張, 則可以拋棄這些對(duì)象。 很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場(chǎng)景。在JDK 1.2之后, Java對(duì)引用的概念進(jìn)行了擴(kuò)充, 將引用分為強(qiáng)引用( StrongReference) 、 軟引用( Soft Reference) 、 弱引用( Weak Reference) 、 虛引用( PhantomReference) 4種, 這4種引用強(qiáng)度依次逐漸減弱。強(qiáng)引用就是指在程序代碼之中普遍存在的, 類似“Object obj=new Object( ) ”這類的引用, 只要強(qiáng)引用還存在, 垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。軟引用是用來(lái)描述一些還有用但并非必需的對(duì)象。 對(duì)于軟引用關(guān)聯(lián)著的對(duì)象, 在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前, 將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。 如果這次回收還沒有足夠的內(nèi)存, 才會(huì)拋出內(nèi)存溢出異常。 在JDK 1.2之后, 提供了SoftReference類來(lái)實(shí)現(xiàn)軟引用。弱引用也是用來(lái)描述非必需對(duì)象的, 但是它的強(qiáng)度比軟引用更弱一些, 被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。 當(dāng)垃圾收集器工作時(shí), 無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。 在JDK 1.2之后, 提供了WeakReference類來(lái)實(shí)現(xiàn)弱引用。虛引用也稱為幽靈引用或者幻影引用, 它是最弱的一種引用關(guān)系。 一個(gè)對(duì)象是否有虛引用的存在, 完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響, 也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。 為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。 在JDK 1.2之后, 提供了PhantomReference類來(lái)實(shí)現(xiàn)虛引用。
通過(guò)這段話我們知道,WeakReference是用來(lái)將這個(gè)類聲明為弱引用的,一個(gè)類如果聲明為弱引用類型,那么這個(gè)類的實(shí)例會(huì)在下一次垃圾回收的時(shí)候被回收。那么這里的Entry為什么要特殊設(shè)置便于垃圾回收呢?
下圖是本文開頭的示例中ThreadLocal對(duì)象被引用的關(guān)系圖:

以本文開頭的用例來(lái)說(shuō),ThreadLocalDemo中有一個(gè)ThreadLocal對(duì)象的引用local,而在線程t1、t2中的ThreadLocalMap中的key也指向了這個(gè)ThreadLocal對(duì)象,如果我在main線程中不需要這個(gè)ThreadLocal對(duì)象了,我人為將靜態(tài)變量local指向?yàn)閚ull,那么這個(gè)ThreadLocal對(duì)象應(yīng)該被回收,因?yàn)閺?qiáng)引用已經(jīng)不在了。但是此時(shí)線程t1、t2中的ThreadLocalMap中的key還引用了這個(gè)ThreadLocal對(duì)象,如果這個(gè)引用是強(qiáng)引用,那么很顯然ThreadLocal對(duì)象將永遠(yuǎn)不會(huì)被回收,直到t1、t2線程終止,如果t1、t2是線程池中的線程,長(zhǎng)期不會(huì)被終止,那么這個(gè)ThreadLocal對(duì)象也就無(wú)法被回收,產(chǎn)生內(nèi)存泄漏。而弱引用就很好的解決了這個(gè)問(wèn)題,因?yàn)檫@個(gè)ThreadLocal對(duì)象的強(qiáng)引用一旦消失,只剩下幾個(gè)弱引用,那么下次垃圾回收的時(shí)候,這個(gè)ThreadLocal對(duì)象還是會(huì)被回收。所以就會(huì)出現(xiàn)上文set的時(shí)候,有的位置有Entry對(duì)象,但是卻沒有key的情況。實(shí)際上,由于弱引用的存在,導(dǎo)致ThreadLocal很多方法都需要去清理這種key為null的Entry。
參考:十分鐘理解Java中的弱引用、ThreadLocal可能引起的內(nèi)存泄露