Java ThreadLocal 深入剖析
最近看Android FrameWork層程式碼,看到了ThreadLocal這個類,有點兒陌生,就翻了各種相關部落格一一拜讀;自己隨後又研究了一遍原始碼,發現自己的理解較之前閱讀的博文有不同之處,所以決定自己寫篇文章說說自己的理解,希望可以起到以下作用:
- 可以疏通研究結果,加深自己的理解
- 可以起到拋磚引玉的作用,幫助感興趣的同學疏通思路
- 分享學習經歷,和大家一起交流和學習
一、 ThreadLocal 是什麼
ThreadLocal 是Java類庫的基礎類,在包java.lang下面;
官方的解釋是這樣的:
Implements a thread-local storage, that is, a variable for which each thread has its own value. All threads share the same ThreadLocal object, but each sees a different value when accessing it, and changes made by one thread do not affect the other threads. The implementation supports null values.
大致意思是:
可以實現執行緒的本地儲存機制,ThreadLocal變數是一個不同執行緒可以擁有不同值的變數。所有的執行緒可以共享同一個ThreadLocal物件,但是不同執行緒訪問的時候可以取得不同的值,而且任意一個執行緒對它的改變不會影響其他執行緒。類實現是支援null值的(可以在set和get方法傳遞和訪問null值)。
概括來講有三個特性:
- 不同執行緒訪問時取得不同的值
- 任意執行緒對它的改變不影響其他執行緒
- 支援null
下面分別對這些特性進行例項驗證,首先定義一個Test類,在此類中我們鑑證上邊所提到的三個特性。類定義如下:
Test.java
public class Test{
//定義ThreadLocal
private static ThreadLocal<String> name;
public static void main(String[] args) throws Exception{
name = new ThreadLocal<String>();
//Define Thread A
Thread a = new Thread(){
public void run (){
System.out.println("Before invoke set,value is:"+name.get());
name.set(“Thread A”);
System.out.println("After invoke set, value is:"+name.get());
}
};
//Define Thread B
Thread b = new Thread(){
public void run(){
System.out.println("Before invoke set,value is :"+name.get());
name.set(“Thread B”);
System.out.println("After invoke set,value is :"+name.get());
}
};
// Not invoke set, print the value is null
System.out.println(name.get());
// Invoke set to fill a value
name.set(“Thread Main”);
// Start thread A
a.start();
a.join();
// Print the value after changed the value by thread A
System.out.println(name.get());
// Start thread B
b.start();
b.join();
// Print the value after changed the value by thread B
System.out.println(name.get())
}
}
程式碼分析:
從定義中我們可以看到只聲明瞭一個ThreadLocal物件,其他三個執行緒(主執行緒、Thread A和Thread B)共享同一個物件;然後,在不同的執行緒中修改物件的值和在不同的執行緒中訪問物件的值,並在控制檯輸出檢視結果。看結果:
從控制檯輸出結果可以看到裡邊有三個null的輸出,這個是因為在輸出前沒有對物件進行賦值,驗證了支援null的特點;再者,還可以發現在每個執行緒我都對物件的值做了修改,但是在其他執行緒訪問物件時並不是修改後的值,而是訪問執行緒本地的值;這樣也驗證了其他兩個特點。
二、 ThreadLocal的作用
大家都知道它的使用場景大都是多執行緒程式設計,至於具體的作用,這個怎麼說那?我覺得這個只能用一個泛的說法來定義,因為一個東西的功能屬性定義了以後會限制大家的思路,就好比說菜刀是用來切菜的,好多人就不會用它切西瓜了。
這裡,說下我對它的作用的認識,僅供參考,希望能有所幫助。這樣來描述吧,當一個多執行緒的程式需要對多數執行緒的部分任務(就是run方法裡的部分程式碼)進行封裝時,在封裝體裡就可以用ThreadLocal來包裝與執行緒相關的成員變數,從而保證執行緒訪問的獨佔性,而且所有執行緒可以共享一個封裝體物件;可以參考下Android裡的Looper。不會用程式碼描述問題的程式設計師不是好程式設計師;
看程式碼:統計執行緒某段程式碼耗時的工具(為說明問題自造)
StatisticCostTime.java
// Class that statistic the cost time
public class StatisticCostTime{
// record the startTime
// private ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private long startTime;
// private ThreadLocal<Long> costTime = new ThreadLocal<Long>();
private long costTime;
private StatisticCostTime(){
}
//Singleton
public static final StatisticCostTime shareInstance(){
return InstanceFactory.instance;
}
private static class InstanceFactory{
private static final StatisticCostTime instance = new StatisticCostTime();
}
// start
public void start(){
// startTime.set(System. nanoTime ());
startTime = System.nanoTime();
}
// end
public void end(){
// costTime.set(System. nanoTime () - startTime.get());
costTime = System.nanoTime() - startTime;
}
public long getStartTime(){
return startTime;
// return startTime.get();
}
public long getCostTime(){
// return costTime.get();
return costTime;
}
好了,工具設計完工了,現在我們用它來統計一下執行緒耗時試試唄:
Main.java
public class Main{
public static void main(String[] args) throws Exception{
// Define the thread a
Thread a = new Thread(){
public void run(){
try{
// start record time
StatisticCostTime.shareInstance().start();
sleep(200);
// print the start time of A
System.out.println("A-startTime:"+StatisticCostTime.shareInstance().getStartTime());
// end the record
StatisticCostTime.shareInstance().end();
// print the costTime of A
System.out.println("A:"+StatisticCostTime.shareInstance().getCostTime());
}catch(Exception e){
}
}
};
// start a
a.start();
// Define thread b
Thread b = new Thread(){
public void run(){
try{
// record the start time of B1
StatisticCostTime.shareInstance().start();
sleep(100);
// print the start time to console
System.out.println("B1-startTime:"+StatisticCostTime.shareInstance().getStartTime());
// end record start time of B1
StatisticCostTime.shareInstance().end();
// print the cost time of B1
System.out.println("B1:"+StatisticCostTime.shareInstance().getCostTime());
// start record time of B2
StatisticCostTime.shareInstance().start();
sleep(100);
// print start time of B2
System.out.println("B2-startTime:"+StatisticCostTime.shareInstance().getStartTime());
// end record time of B2
StatisticCostTime.shareInstance().end();
// print cost time of B2
System.out.println("B2:"+StatisticCostTime.shareInstance().getCostTime());
}catch(Exception e){
}
}
};
b.start();
}
}
執行程式碼後輸出結果是這樣的
注意:輸出結果精確度為納秒級
看結果是不是和我們預想的不一樣,發現A的結果應該約等於B1+B2才對呀,怎麼變成和B2一樣了那?答案就是我們在定義startTime和costTime變數時,本意是不應共享的,應是執行緒獨佔的才對。而這裡變數隨單例共享了,所以當計算A的值時,其實startTime已經被B2修改了,所以就輸出了和B2一樣的結果。
現在我們把StatisticCostTime中註釋掉的部分開啟,換成ThreadLocal的宣告方式試下。
看結果:
呀!這下達到預期效果了,這時候有同學會說這不是可以執行緒併發訪問了嗎,是不是隻要我用了ThreadLocal就可以保證執行緒安全了?答案是no!首先先弄明白為什麼會有執行緒安全問題,無非兩種情:
1、不該共享的資源,你線上程間共享了;
2、執行緒間共享的資源,你沒有保證有序訪問;
前者可以用“空間換時間”的方式解決,用ThreadLocal(也可以直接宣告執行緒區域性變數),後者用“時間換空間”的方式解決,顯然這個就不是ThreadLocal力所能及的了。
三、 ThreadLocal 原理
實現原理其實很簡單,每次對ThreadLocal 物件的讀寫操作其實是對執行緒的Values物件的讀寫操作;這裡澄清一下,沒有什麼變數副本的建立,因為就沒有用變數分配的記憶體空間來存T物件的,而是用它所線上程的Values來存T物件的;我們線上程中每次呼叫ThreadLocal的set方法時,實際上是將object寫入執行緒對應Values物件的過程;呼叫ThreadLocal的get方法時,實際上是從執行緒對應Values物件取object的過程。
看原始碼:
ThreadLocal 的成員變數set
/**
* Sets the value of this variable for the current thread. If set to
* {@code null}, the value will be set to null and the underlying entry will
* still be present.
*
* @param value the new value of the variable for the caller thread.
*/
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
TreadLocal 的成員方法get
/**
* Returns the value of this variable for the current thread. If an entry
* doesn't yet exist for this variable on this thread, this method will
* create an entry, populating the value with the result of
* {@link #initialValue()}.
*
* @return the current value of the variable for the calling thread.
*/
@SuppressWarnings("unchecked")
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this);
}
ThreadLocal的成員方法initializeValues
/**
* Creates Values instance for this thread and variable type.
*/
Values initializeValues(Thread current) {
return current.localValues = new Values();
}
ThreadLocal 的成員方法values
/**
* Gets Values instance for this thread and variable type.
*/
Values values(Thread current) {
return current.localValues;
}
那這個Values又是怎樣讀寫Object那?
Values是作為ThreadLocal的內部類存在的;這個Values裡包括了一個重要陣列Object[],這個資料就是解答問題的關鍵部分,它是用來儲存執行緒本地各種型別TreadLocal變數用的;那麼問題來了,具體取某個型別的變數時是怎麼保證不取到其他型別的值那?按一般的做法會用一個Map根據key-value對映一下的;對的,思路就是這個思路,但是這裡並沒有用Map來實現,是用一個Object[]實現的Map機制;但是,若要用Map理解的話,也是不可以的,因為機制是相同的;key其實上對應ThreadLocal的弱引用,value就對應我們傳進去的Object。
解釋下是怎麼用Object[]實現Map機制的(參考圖1);它是用陣列下標的奇偶來區分key和value的,就是下表是偶數的位置儲存key,奇數儲存value,就是這樣搞得;感興趣的同學如果想知道演算法實現的話,可以深入研究一下,這裡我不在詳述了。
結合前面第一個例項分析下儲存情況:
當程式執行時存在A,B和main三個執行緒,分別線上程中呼叫name.set()時同時針對三個執行緒例項在堆區分配了三塊相同的記憶體空間來儲存Values物件,以name引用作為key,具體的object作為值存進三個不同的Object[](參看下圖):
四、 總結
ThreadLocal 不能完全解決多執行緒程式設計時的併發問題,這種問題還要根據不同的情況選擇不同的解決方案,“空間換時間”還是“時間換空間”。
ThreadLocal最大的作用就是把執行緒共享變數轉換成執行緒本地變數,實現執行緒之間的隔離。