摘要:
ThreadLocal 又名線程局部變量,是 Java 中一種較為特殊的線程綁定機(jī)制铛铁,用于保證變量在不同線程間的隔離性,以方便每個(gè)線程處理自己的狀態(tài)却妨。進(jìn)一步地避归,本文以ThreadLocal類的源碼為切入點(diǎn),深入分析了ThreadLocal類的作用原理管呵,并給出應(yīng)用場(chǎng)景和一般使用步驟梳毙。
一. 對(duì) ThreadLocal 的理解
1). ThreadLocal 概述
ThreadLocal 又名 線程局部變量 ,是 Java 中一種較為特殊的線程綁定機(jī)制捐下,可以為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本账锹,并且每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)與其它線程的副本發(fā)生沖突坷襟。一般而言奸柬,通過(guò) ThreadLocal 存取的數(shù)據(jù)總是與當(dāng)前線程相關(guān),也就是說(shuō)婴程,JVM 為每個(gè)運(yùn)行的線程綁定了私有的本地實(shí)例存取空間廓奕,從而為多線程環(huán)境常出現(xiàn)的并發(fā)訪問(wèn)問(wèn)題提供了一種 隔離機(jī)制 。
如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享档叔,那就看看這些共享數(shù)據(jù)的代碼能否保證在同一個(gè)線程中執(zhí)行桌粉?如果能保證,我們就可以把共享數(shù)據(jù)的可見(jiàn)范圍限制在同一個(gè)線程之內(nèi)衙四,這樣铃肯,無(wú)須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭(zhēng)用的問(wèn)題。也就是說(shuō)传蹈,如果一個(gè)某個(gè)變量要被某個(gè)線程 獨(dú)享押逼,那么我們就可以通過(guò)ThreadLocal來(lái)實(shí)現(xiàn)線程本地存儲(chǔ)功能步藕。
2). ThreadLocal 在 JDK 中的定義
ThreadLocal
This class provides thread-local variables. These variables differ from their normal counterparts(副本) in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread ( e.g., a user ID or Transaction ID ). (如果我們希望通過(guò)將某個(gè)類的狀態(tài)(例如用戶ID、事務(wù)ID)與線程關(guān)聯(lián)起來(lái)挑格,那么通常在這個(gè)類中定義private static類型的ThreadLocal 實(shí)例咙冗。)
Each thread holds an implicit reference to its copy of a thread-local variable (見(jiàn)下圖) as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
我們可以從中摘出三條要點(diǎn):
- 每個(gè)線程都有關(guān)于該 ThreadLocal變量 的私有值
每個(gè)線程都有一個(gè)獨(dú)立于其他線程的上下文來(lái)保存這個(gè)變量的值,并且對(duì)其他線程是不可見(jiàn)的漂彤。 - 獨(dú)立于變量的初始值
ThreadLocal 可以給定一個(gè)初始值雾消,這樣每個(gè)線程就會(huì)獲得這個(gè)初始化值的一個(gè)拷貝,并且每個(gè)線程對(duì)這個(gè)值的修改對(duì)其他線程是不可見(jiàn)的显歧。 - 將某個(gè)類的狀態(tài)與線程相關(guān)聯(lián)
我們從JDK中對(duì)ThreadLocal的描述中可以看出,ThreadLocal的一個(gè)重要作用是就是將類的狀態(tài)與線程關(guān)聯(lián)起來(lái)确镊,這個(gè)時(shí)候通常的解決方案就是在這個(gè)類中定義一個(gè) private static ThreadLocal 實(shí)例士骤。
3). 應(yīng)用場(chǎng)景
類 ThreadLocal 主要解決的就是為每個(gè)線程綁定自己的值,以方便其處理自己的狀態(tài)蕾域。形象地講拷肌,可以將 ThreadLocal變量 比喻成全局存放數(shù)據(jù)的盒子,盒子中可以存儲(chǔ)每個(gè)線程的私有數(shù)據(jù)旨巷。例如巨缘,以下類用于生成對(duì)每個(gè)線程唯一的局部標(biāo)識(shí)符。線程 ID 是在第一次調(diào)用 uniqueNum.get() 時(shí)分配的采呐,在后續(xù)調(diào)用中不會(huì)更改若锁。
import java.util.concurrent.atomic.AtomicInteger;
public class UniqueThreadIdGenerator {
private static final AtomicInteger uniqueId = new AtomicInteger(0);
private static final ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return uniqueId.getAndIncrement();
}
};
public static void main(String[] args) {
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
String name = "Thread-" + i;
threads[i] = new Thread(name){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": "
+ uniqueNum.get());
}
};
threads[i].start();
}
System.out.println(Thread.currentThread().getName() + ": "
+ uniqueNum.get());
}
}/* Output(輸出結(jié)果不唯一):
Thread-1: 2
Thread-0: 0
Thread-2: 3
main: 1
Thread-3: 4
Thread-4: 5
*///:~
二. 深入分析ThreadLocal類
下面,我們來(lái)看一下 ThreadLocal 的具體實(shí)現(xiàn)斧吐,該類一共提供的四個(gè)方法:
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }1234
其中又固,get()方法是用來(lái)獲取 ThreadLocal變量 在當(dāng)前線程中保存的值,set() 用來(lái)設(shè)置 ThreadLocal變量 在當(dāng)前線程中的值煤率,remove() 用來(lái)移除當(dāng)前線程中相關(guān) ThreadLocal變量仰冠,initialValue() 是一個(gè) protected 方法,一般需要重寫蝶糯。
1洋只、 原理探究
1). 切入點(diǎn):get()
首先,我們先看其源碼:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread(); // 獲取當(dāng)前線程對(duì)象
ThreadLocalMap map = getMap(t); // 獲取當(dāng)前線程的成員變量 threadLocals
if (map != null) {
// 從當(dāng)前線程的 ThreadLocalMap 獲取該 thread-local variable 對(duì)應(yīng)的 entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value; // 取得目標(biāo)值
}
return setInitialValue();
}
2).關(guān)鍵點(diǎn):setInitialValue()
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue(); // 默認(rèn)實(shí)現(xiàn)返回 null
Thread t = Thread.currentThread(); // 獲得當(dāng)前線程
ThreadLocalMap map = getMap(t); // 得到當(dāng)前線程 ThreadLocalMap類型域 threadLocals
if (map != null)
map.set(this, value); // 該 map 的鍵是當(dāng)前 ThreadLocal 對(duì)象
else
createMap(t, value);
return value;
}
我們緊接著看上述方法涉及到的三個(gè)方法:initialValue()昼捍,set(this, value) 和 createMap(t, value)识虚。
(1) initialValue()
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the <tt>initialValue</tt> method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns <tt>null</tt>; if the
* programmer desires thread-local variables to have an initial
* value other than <tt>null</tt>, <tt>ThreadLocal</tt> must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null; // 默認(rèn)實(shí)現(xiàn)返回 null
}
(2) createMap()
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue); // this 指代當(dāng)前 ThreadLocal 變量,為 map 的鍵
}
至此妒茬,可能大部分朋友已經(jīng)明白了 ThreadLocal類 是如何為每個(gè)線程創(chuàng)建變量的副本的:
① 每個(gè)線程內(nèi)部有一個(gè) ThreadLocal.ThreadLocalMap 類型的成員變量 threadLocals舷礼,這個(gè) threadLocals 存儲(chǔ)了與該線程相關(guān)的所有 ThreadLocal 變量及其對(duì)應(yīng)的值(”ThreadLocal 變量及其對(duì)應(yīng)的值” 就是該Map中的一個(gè) Entry)。我們知道郊闯,Map 中存放的是一個(gè)個(gè) Entry妻献,其中每個(gè) Entry 都包含一個(gè) Key 和一個(gè) Value蛛株。在這里,就threadLocals 而言育拨,它的 Entry 的 Key 是 ThreadLocal 變量谨履, Value 是該 ThreadLocal 變量對(duì)應(yīng)的值;
② 初始時(shí)熬丧,在Thread里面笋粟,threadLocals為空,當(dāng)通過(guò)ThreadLocal變量調(diào)用get()方法或者set()方法析蝴,就會(huì)對(duì)Thread類中的threadLocals進(jìn)行初始化害捕,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的值為value闷畸,存到 threadLocals尝盼;
③ 然后在當(dāng)前線程里面,如果要使用ThreadLocal對(duì)象佑菩,就可以通過(guò)get方法獲得該線程的threadLocals盾沫,然后以該ThreadLocal對(duì)象為鍵取得其對(duì)應(yīng)的 Value,也就是ThreadLocal對(duì)象中所存儲(chǔ)的值殿漠。
2赴精、實(shí)例驗(yàn)證
下面通過(guò)一個(gè)例子來(lái)證明通過(guò)ThreadLocal能達(dá)到在每個(gè)線程中創(chuàng)建變量副本的效果:
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
test.set();
System.out.println("父線程 main :");
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread() {
public void run() {
test.set();
System.out.println("\n子線程 Thread-0 :");
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
}
}/* Output:
父線程 main :
1
main
子線程 Thread-0 :
12
Thread-0
*///:~
從這段代碼的輸出結(jié)果可以看出,在main線程中和thread1線程中绞幌,longLocal保存的副本值和stringLocal保存的副本值都不一樣蕾哟,并且進(jìn)一步得出:
實(shí)際上,通過(guò) ThreadLocal 創(chuàng)建的副本是存儲(chǔ)在每個(gè)線程自己的threadLocals中的莲蜘;
為何 threadLocals 的類型 ThreadLocalMap 的鍵值為 ThreadLocal 對(duì)象渐苏,因?yàn)槊總€(gè)線程中可有多個(gè) threadLocal變量,就像上面代碼中的 longLocal 和 stringLocal菇夸;
在進(jìn)行g(shù)et之前琼富,必須先set,否則會(huì)報(bào)空指針異常庄新;若想在get之前不需要調(diào)用set就能正常訪問(wèn)的話鞠眉,必須重寫initialValue()方法。
三. ThreadLocal的應(yīng)用場(chǎng)景
在 Java 中择诈,類 ThreadLocal 解決的是變量在不同線程間的隔離性械蹋。最常見(jiàn)的 ThreadLocal 使用場(chǎng)景有 數(shù)據(jù)庫(kù)連接問(wèn)題、Session管理等羞芍。
(1) 數(shù)據(jù)庫(kù)連接問(wèn)題
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
(2) Session管理
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
(3) Thread-per-Request (一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)服務(wù)器線程)
在經(jīng)典Web交互模型中哗戈,請(qǐng)求的處理基本上采用的都是“一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)服務(wù)器線程”的處理方式,因此就可以將請(qǐng)求設(shè)置成類似ThreadLocal<Request>的形式荷科,這樣唯咬,當(dāng)某個(gè)服務(wù)器線程來(lái)處理請(qǐng)求時(shí)纱注,就可以獨(dú)享該請(qǐng)求的處理了。
四. ThreadLocal 一般使用步驟
ThreadLocal 使用步驟一般分為三步:
- 創(chuàng)建一個(gè) ThreadLocal 對(duì)象 threadXxx胆胰,用來(lái)保存線程間需要隔離處理的對(duì)象 xxx狞贱;
- 提供一個(gè)獲取要隔離訪問(wèn)的數(shù)據(jù)的方法 getXxx(),在方法中判斷蜀涨,若 ThreadLocal對(duì)象為null時(shí)候瞎嬉,應(yīng)該 new() 一個(gè)隔離訪問(wèn)類型的對(duì)象;
- 在線程類的run()方法中厚柳,通過(guò)getXxx()方法獲取要操作的數(shù)據(jù)氧枣,這樣可以保證每個(gè)線程對(duì)應(yīng)一個(gè)數(shù)據(jù)對(duì)象,在任何時(shí)刻都操作的是這個(gè)對(duì)象别垮,不會(huì)交叉便监。