五、你有我有全都有—— ThreadLocal如何解決並發安全性?
前面我們介紹了Java當中多個線程搶占一個共享資源的問題。但不論是同步還是重入鎖,都不能實實在在的解決資源緊缺的情況,這些 方案只是靠制定規則來約束線程的行為,讓它們不再拼命的爭搶,而不是真正從實質上解決他們對資源的需求。
在JDK 1.2當中,引入了java.lang.ThreadLocal。它為我們提供了一種全新的思路來解決線程並發的問題。但是他的名字難免讓我們望 文生義:本地線程?
什麼是本地線程?
本地線程開玩笑的說:不要迷戀哥,哥只是個傳說。
其實ThreadLocal並非Thread at Local,而是LocalVariable in a Thread。
根據WikiPedia上的介紹,ThreadLocal其實是源於一項多線程技術,叫做Thread Local Storage,即線程本地存儲技術。不僅僅是Java ,在C++、C#、.NET、Python、Ruby、Perl等開發平台上,該技術都已經得以實現。
當使用ThreadLocal維護變量時,它會為每個使用該變量的線程提供獨立的變量副本。也就是說,他從根本上解決的是資源數量的問題 ,從而使得每個線程持有相對獨立的資源。這樣,當多個線程進行工作的時候,它們不需要糾結於同步的問題,於是性能便大大提升。但 資源的擴張帶來的是更多的空間消耗,ThreadLocal就是這樣一種利用空間來換取時間的解決方案。
說了這麼多,來看看如何正確使用ThreadLocal。
通過研究JDK文檔,我們知道,ThreadLocal中有幾個重要的方法:get()、set()、remove()、initailValue(),對應的含義分別是:
返回此線程局部變量的當前線程副本中的值、將此線程局部變量的當前線程副本中的值設置為指定值、移除此線程局部變量當前線程的 值、返回此線程局部變量的當前線程的“初始值”。
還記得我們在第三篇的上半節引出的那個例子麼?幾個線程修改同一個Student對象中的age屬性。為了保證這幾個線程能夠工作正常, 我們需要對Student的對象進行同步。
下面我們對這個程序進行一點小小的改造,我們通過繼承Thread來實現多線程:
/**
*
* @author x-spirit
*/
public class ThreadDemo3 extends Thread{
private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>();
public ThreadDemo3(Student stu){
stuLocal.set(stu);
}
public static void main(String[] args) {
Student stu = new Student();
ThreadDemo3 td31 = new ThreadDemo3(stu);
ThreadDemo3 td32 = new ThreadDemo3(stu);
ThreadDemo3 td33 = new ThreadDemo3(stu);
td31.start();
td32.start();
td33.start();
}
@Override
public void run() {
accessStudent();
}
public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running!");
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread " + currentThreadName + " set age to:" + age);
Student student = stuLocal.get();
student.setAge(age);
System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());
}
}
貌似這個程序沒什麼問題。但是運行結果卻顯示:這個程序中的3個線程會拋出3個空指針異常。讀者一定感到很困惑。我明明在構造器 當中把Student對象 set進了ThreadLocal裡面阿,為什麼run起來之後居然在調用stuLocal.get()方法的時候得到的是NULL呢?
帶著這個疑問,讓我們深入到JDK的代碼當中,去一看究竟。
原來,在ThreadLocal中,有一個內部類叫做ThreadLocalMap。這個ThreadLocalMap並非java.util.Map的一個實現,而是利用 java.lang.ref.WeakReference實現的一個鍵-值對應的數據結構其中,key是ThreadLocal類型,而value是Object類型,我們可以簡單的視 為HashMap<ThreadLocal,Object>。
而在每一個Thread對象中,都有一個ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的 set方法就是首先嘗試從當前 線程中取得ThreadLocalMap(以下簡稱Map)對象。如果取到的不為null,則以ThreadLocal對象自身為key,來取Map中的value。如果取不 到Map對象,則首先為當前線程創建一個ThreadLocalMap,然後以ThreadLocal 對象自身為key,將傳入的value放入該Map中。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
而get方法則是首先得到當前線程的ThreadLocalMap對象,然後,根據ThreadLocal對象自身,取出相應的value。當然,如果在當前線 程中取不到ThreadLocalMap對象,則嘗試為當前線程創建ThreadLocalMap對象,並以ThreadLocal對象自身為 key,把initialValue()方法 產生的對象作為value放入新創建的ThreadLocalMap中。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
這樣,我們就明白上面的問題出在哪裡:我們在main方法執行期間,試圖在調用ThreadDemo3的構造器時向ThreadLocal置入 Student對 象,而此時,以ThreadLocal對象為key,Student對象為value的Map是被放入當前的活動線程內的。也就是 Main線程。而當我們的3個 ThreadDemo3線程運行起來以後,調用get()方法,都是試圖從當前的活動線程中取得 ThreadLocalMap對象,但當前的活動線程顯然已經不 是Main線程了,於是,程序最終執行了ThreadLocal原生的 initialValue()方法,返回了null。
講到這裡,我想不少朋友一定已經看出來了:ThreadLocal的initialValue()方法是需要被覆蓋的。
於是,ThreadLocal的正確使用方法是:將ThreadLocal以內部類的形式進行繼承,並覆蓋原來的initialValue()方法,在這裡產生可供 線程擁有的本地變量值。
這樣,我們就有了下面的正確例程:
/**
*
* @author x-spirit
*/
public class ThreadDemo3 extends Thread{
private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(){
@Override
protected Student initialValue() {
return new Student();
}
};
public ThreadDemo3(){
}
public static void main(String[] args) {
ThreadDemo3 td31 = new ThreadDemo3();
ThreadDemo3 td32 = new ThreadDemo3();
ThreadDemo3 td33 = new ThreadDemo3();
td31.start();
td32.start();
td33.start();
}
@Override
public void run() {
accessStudent();
}
public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running!");
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread " + currentThreadName + " set age to:" + age);
Student student = stuLocal.get();
student.setAge(age);
System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());
}
}
********** 補疑 ******************
有的童鞋可能會問:“你這個Demo根本沒體現出來,每個線程裡都有一個ThreadLocal對象;應該是一個ThreadLocal對象對應多個線程 ,你這變成了一對一,完全沒體現出ThreadLocal的作用。”
那麼我們來看一下如何用一個ThreadLocal對象來對應多個線程:
/** *//**
*
* @author x-spirit
*/
public class ThreadDemo3 implements Runnable{
private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(){
@Override
protected Student initialValue() {
return new Student();
}
};
public ThreadDemo3(){
}
public static void main(String[] args) {
ThreadDemo3 td3 = new ThreadDemo3();
Thread t1 = new Thread(td3);
Thread t2 = new Thread(td3);
Thread t3 = new Thread(td3);
t1.start();
t2.start();
t3.start();
}
@Override
public void run() {
accessStudent();
}
public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running!");
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread " + currentThreadName + " set age to:" + age);
Student student = stuLocal.get();
student.setAge(age);
System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());
}
}
這裡,多個線程對象都使用同一個實現了Runnable接口的ThreadDemo3對象來構造。這樣,多個線程使用的ThreadLocal對象就是同一個 。結果仍然是正確的。但是仔細回想一下,這兩種實現方案有什麼不同呢?
答案其實很簡單,並沒有本質上的不同。對於第一種實現,不同的線程對象當中ThreadLocalMap裡面的KEY使用的是不同的 ThreadLocal對象。而對於第二種實現,不同的線程對象當中ThreadLocalMap裡面的KEY是同一個ThreadLocal對象。但是從本質上講,不同 的線程對象都是利用其自身的ThreadLocalMap對象來對各自的Student對象進行封裝,用ThreadLocal對象作為該ThreadLocalMap的KEY。所 以說,“ThreadLocal的思想精髓就是為每個線程創建獨立的資源副本。”這句話並不應當被理解成:一定要使用同一個ThreadLocal對象 來對多個線程進行處理。因為真正用來封裝變量的不是ThreadLocal。就算是你的程序中所有線程都共用同一個ThreadLocal對象,而你真 正封裝到ThreadLocalMap中去的仍然是.hashCode()方法返回不同值的不同對象。就好比線程就是房東,ThreadLocalMap就是房東的房子。 房東通過ThreadLocal這個中介去和房子裡的房客打交道,而房東不管要讓房客住進去還是搬出來,都首先要經過ThreadLocal這個中介。
所以提到ThreadLocal,我們不應當顧名思義的認為JDK裡面提供ThreadLocal就是提供了一個用來封裝本地線程存儲的容器,它本身並 沒有Map那樣的容器功能。真正發揮作用的是ThreadLocalMap。也就是說,事實上,采用ThreadLocal來提高並發行,首先要理解,這不是 一種簡單的對象封裝,而是一套機制,而這套機制中的三個關鍵因素(Thread、ThreadLocal、ThreadLocalMap)之間的關系是值得我們引 起注意的。
**************** 補疑完畢 ***************************
可見,要正確使用ThreadLocal,必須注意以下幾點:
1. 總是對ThreadLocal中的initialValue()方法進行覆蓋。
2. 當使用set()或get()方法時牢記這兩個方法是對當前活動線程中的ThreadLocalMap進行操作,一定要認清哪個是當前活動線程!
3. 適當的使用泛型,可以減少不必要的類型轉換以及可能由此產生的問題。
運行該程序,我們發現:程序的執行過程只需要5秒,而如果采用同步的方法,程序的執行結果相同,但執行時間需要15秒。以前是多 個線程為了爭取一個資源,不得不在同步規則的制約下互相謙讓,浪費了一些時間。
現在,采用ThreadLocal機制以後,可用的資源多了,你有我有全都有,所以,每個線程都可以毫無顧忌的工作,自然就提高了並發性 ,線程安全也得以保證。
當今很多流行的開源框架也采用ThreadLocal機制來解決線程的並發問題。比如大名鼎鼎的 Struts 2.x 和 Spring 等。
把ThreadLocal這樣的話題放在我們的同步機制探討中似乎顯得不是很合適。但是ThreadLocal的確為我們解決多線程的並發問題帶來了 全新的思路。它為每個線程創建一個獨立的資源副本,從而將多個線程中的數據隔離開來,避免了同步所產生的性能問題,是一種“以空 間換時間”的解決方案。
但這並不是說ThreadLocal就是包治百病的萬能藥了。如果實際的情況不允許我們為每個線程分配一個本地資源副本的話,同步還是非 常有意義的。
好了,本系列到此馬上就要劃上一個圓滿的句號了。不知大家有什麼意見和疑問沒有。希望看到你們的留言。
下一講中我們就來對之前的內容進行一個總結,順便討論一下被遺忘的volatile關鍵字。敬請期待。