多線程編程詳解

線程的同步
由於同一進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問衝突這個嚴重的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個數據對象被多個線程同時訪問。
由於我們可以通過 private 關鍵字來保證數據對象只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是 synchronized 關鍵字,它包括兩種用法:synchronized 方法和 synchronized 塊。
1. synchronized 方法:通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。如:
public synchronized void accessVal(int newVal);
synchronized 方法控制對類成員變量的訪問:每個類實例對應一把鎖,每個 synchronized 方法都必須獲得調用該方法的類實例的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。這種機制確保了同一時刻對於每一個類實例,其所有聲明爲 synchronized 的成員函數中至多隻有一個處於可執行狀態(因爲至多隻有一個能夠獲得該類實例對應的鎖),從而有效避免了類成員變量的訪問衝突(只要所有可能訪問類成員變量的方法均被聲明爲 synchronized)。
在 Java 中,不光是類實例,每一個類也對應一把鎖,這樣我們也可將類的靜態成員函數聲明爲 synchronized ,以控制其對類的靜態成員變量的訪問。
synchronized 方法的缺陷:若將一個大的方法聲明爲synchronized 將會大大影響效率,典型地,若將線程類的方法 run() 聲明爲 synchronized ,由於在線程的整個生命期內它一直在運行,因此將導致它對本類任何 synchronized 方法的調用都永遠不會成功。當然我們可以通過將訪問類成員變量的代碼放到專門的方法中,將其聲明爲 synchronized ,並在主方法中調用來解決這一問題,但是 Java 爲我們提供了更好的解決辦法,那就是 synchronized 塊。
2. synchronized 塊:通過 synchronized關鍵字來聲明synchronized 塊。語法如下:
synchronized(syncObject) {
//允許訪問控制的代碼 }
synchronized 塊是這樣一個代碼塊,其中的代碼必須獲得對象 syncObject (如前所述,可以是類實例或類)的鎖方能執行,具體機制同前所述。由於可以針對任意代碼塊,且可任意指定上鎖的對象,故靈活性較高。
六:線程的阻塞
爲了解決對共享存儲區的訪問衝突,Java 引入了同步機制,現在讓我們來考察多個線程對共享資源的訪問,顯然同步機制已經不夠了,因爲在任意時刻所要求的資源不一定已經準備好了被訪問,反過來,同一時刻準備好了的資源也可能不止一個。爲了解決這種情況下的訪問控制問題,Java 引入了對阻塞機制的支持。
阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒),學過操作系統的同學對它一定已經很熟悉了。Java 提供了大量方法來支持阻塞,下面讓我們逐一分析。
1. sleep() 方法:sleep() 允許 指定以毫秒爲單位的一段時間作爲參數,它使得線程在指定的時間內進入阻塞狀態,不能得到CPU 時間,指定的時間一過,線程重新進入可執行狀態。
典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓線程阻塞一段時間後重新測試,直到條件滿足爲止。
2. suspend() 和 resume() 方法:兩個方法配套使用,suspend()使得線程進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被調用,才能使得線程重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個線程產生的結果的情形:測試發現結果還沒有產生後,讓線程阻塞,另一個線程產生了結果後,調用 resume() 使其恢復。
3. yield() 方法:yield() 使得線程放棄當前分得的 CPU 時間,但是不使線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程序認爲該線程已執行了足夠的時間從而轉到另一個線程。
4. wait() 和 notify() 方法:兩個方法配套使用,wait() 使得線程進入阻塞狀態,它有兩種形式,一種允許 指定以毫秒爲單位的一段時間作爲參數,另一種沒有參數,前者當對應的 notify() 被調用或者超出指定時間時線程重新進入可執行狀態,後者則必須對應的 notify() 被調用。
初看起來它們與 suspend() 和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的所有方法,阻塞時都不會釋放佔用的鎖(如果佔用了的話),而這一對方法則相反。上述的核心區別導致了一系列的細節上的區別。
首先,前面敘述的所有方法都隸屬於 Thread 類,但是這一對卻直接隸屬於 Object 類,也就是說,所有對象都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因爲這一對方法阻塞時要釋放佔用的鎖,而鎖是任何對象都具有的,調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。而調用 任意對象的notify()方法則導致因調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。
其次,前面敘述的所有方法都可在任何位置調用,但是這一對方法卻必須在 synchronized 方法或塊中調用,理由也很簡單,只有在synchronized 方法或塊中當前線程才佔有鎖,纔有鎖可以釋放。同樣的道理,調用這一對方法的對象上的鎖必須爲當前線程所擁有,這樣纔有鎖可以釋放。因此,這一對方法調用必須放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖對象就是調用這一對方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現IllegalMonitorStateException 異常。
wait() 和 notify() 方法的上述特性決定了它們經常和synchronized 方法或塊一起使用,將它們和操作系統的進程間通信機制作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於操作系統原語的功能,它們的執行不會受到多線程機制的干擾,而這一對方法則相當於 block 和wakeup 原語(這一對方法均聲明爲 synchronized)。它們的結合使得我們可以實現操作系統上一系列精妙的進程間通信的算法(如信號量算法),並用於解決各種複雜的線程間通信問題。
關於 wait() 和 notify() 方法最後再說明兩點:
第一:調用 notify() 方法導致解除阻塞的線程是從因調用該對象的 wait() 方法而阻塞的線程中隨機選取的,我們無法預料哪一個線程將會被選擇,所以編程時要特別小心,避免因這種不確定性而產生問題。
第二:除了 notify(),還有一個方法 notifyAll() 也可起到類似作用,唯一的區別在於,調用 notifyAll() 方法將把因調用該對象的 wait() 方法而阻塞的所有線程一次性全部解除阻塞。當然,只有獲得鎖的那一個線程才能進入可執行狀態。
談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定超時期限的 wait() 方法的調用都可能產生死鎖。遺憾的是,Java 並不在語言級別上支持死鎖的避免,我們在編程中必須小心地避免死鎖。
以上我們對 Java 中實現線程阻塞的各種方法作了一番分析,我們重點分析了 wait() 和 notify() 方法,因爲它們的功能最強大,使用也最靈活,但是這也導致了它們的效率較低,較容易出錯。實際使用中我們應該靈活使用各種方法,以便更好地達到我們的目的。
七:守護線程
守護線程是一類特殊的線程,它和普通線程的區別在於它並不是應用程序的核心部分,當一個應用程序的所有非守護線程終止運行時,即使仍然有守護線程在運行,應用程序也將終止,反之,只要有一個非守護線程在運行,應用程序就不會終止。守護線程一般被用於在後臺爲其它線程提供服務。
可以通過調用方法 isDaemon() 來判斷一個線程是否是守護線程,也可以調用方法 setDaemon() 來將一個線程設爲守護線程。
八:線程組
線程組是一個 Java 特有的概念,在 Java 中,線程組是類ThreadGroup 的對象,每個線程都隸屬於唯一一個線程組,這個線程組在線程創建時指定並在線程的整個生命期內都不能更改。你可以通過調用包含 ThreadGroup 類型參數的 Thread 類構造函數來指定線程屬的線程組,若沒有指定,則線程缺省地隸屬於名爲 system 的系統線程組。
在 Java 中,除了預建的系統線程組外,所有線程組都必須顯式創建。在 Java 中,除系統線程組外的每個線程組又隸屬於另一個線程組,你可以在創建線程組時指定其所隸屬的線程組,若沒有指定,則缺省地隸屬於系統線程組。這樣,所有線程組組成了一棵以系統線程組爲根的樹。
Java 允許我們對一個線程組中的所有線程同時進行操作,比如我們可以通過調用線程組的相應方法來設置其中所有線程的優先級,也可以啓動或阻塞其中的所有線程。
Java 的線程組機制的另一個重要作用是線程安全。線程組機制允許我們通過分組來區分有不同安全特性的線程,對不同組的線程進行不同的處理,還可以通過線程組的分層結構來支持不對等安全措施的採用。Java 的 ThreadGroup 類提供了大量的方法來方便我們對線程組樹中的每一個線程組以及線程組中的每一個線程進行操作。
九:總結
在這一講中,我們一起學習了 Java 多線程編程的方方面面,包括創建線程,以及對多個線程進行調度、管理。我們深刻認識到了多線程編程的複雜性,以及線程切換開銷帶來的多線程程序的低效性,這也促使我們認真地思考一個問題:我們是否需要多線程?何時需要多線程?
多線程的核心在於多個代碼塊併發執行,本質特點在於各代碼塊之間的代碼是亂序執行的。我們的程序是否需要多線程,就是要看這是否也是它的內在特點。
假如我們的程序根本不要求多個代碼塊併發執行,那自然不需要使用多線程;假如我們的程序雖然要求多個代碼塊併發執行,但是卻不要求亂序,則我們完全可以用一個循環來簡單高效地實現,也不需要使用多線程;只有當它完全符合多線程的特點時,多線程機制對線程間通信和線程管理的強大支持纔能有用武之地,這時使用多線程纔是值得的。
  線程的(同步)控制
  一個Java程序的多線程之間可以共享數據。當線程以異步方式訪問共享數據時,有時候是不安全的或者不和邏輯的。比如,同一時刻一個線程在讀取數據,另外一個線程在處理數據,當處理數據的線程沒有等到讀取數據的線程讀取完畢就去處理數據,必然得到錯誤的處理結果。這和我們前面提到的讀取數據和處理數據並行多任務並不矛盾,這兒指的是處理數據的線程不能處理當前還沒有讀取結束的數據,但是可以處理其它的數據。
  如果我們採用多線程同步控制機制,等到第一個線程讀取完數據,第二個線程才能處理該數據,就會避免錯誤。可見,線程同步是多線程編程的一個相當重要的技術。
  在講線程的同步控制前我們需要交代如下概念:
  1 用Java關鍵字synchonized同步對共享數據操作的方法
  在一個對象中,用synchonized聲明的方法爲同步方法。Java中有一個同步模型-監視器,負責管理線程對對象中的同步方法的訪問,它的原理是:賦予該對象唯一一把'鑰匙',當多個線程進入對象,只有取得該對象鑰匙的線程纔可以訪問同步方法,其它線程在該對象中等待,直到該線程用wait()方法放棄這把鑰匙,其它等待的線程搶佔該鑰匙,搶佔到鑰匙的線程後纔可得以執行,而沒有取得鑰匙的線程仍被阻塞在該對象中等待。
  file://聲明同步的一種方式:將方法聲明同步
  class store
   {  public synchonized void store_in()
    {    ….    }
    public synchonized void store_out(){ ….}
    }
  2 利用wait()、notify()及notifyAll()方法發送消息實現線程間的相互聯繫
  Java程序中多個線程通過消息來實現互動聯繫的,這幾種方法實現了線程間的消息發送。例如定義一個對象的synchonized 方法,同一時刻只能夠有一個線程訪問該對象中的同步方法,其它線程被阻塞。通常可以用notify()或notifyAll()方法喚醒其它一個或所有線程。而使用wait()方法來使該線程處於阻塞狀態,等待其它的線程用notify()喚醒。
  一個實際的例子就是生產和銷售,生產單元將產品生產出來放在倉庫中,銷售單元則從倉庫中提走產品,在這個過程中,銷售單元必須在倉庫中有產品時才能提貨;如果倉庫中沒有產品,則銷售單元必須等待。
  程序中,假如我們定義一個倉庫類store,該類的實例對象就相當於倉庫,在store類中定義兩個成員方法:store_in(),用來模擬產品製造者往倉庫中添加產品;strore_out()方法則用來模擬銷售者從倉庫中取走產品。然後定義兩個線程類:customer類,其中的run()方法通過調用倉庫類中的store_out()從倉庫中取走產品,模擬銷售者;另外一個線程類producer中的run()方法通過調用倉庫類中的store_in()方法向倉庫添加產品,模擬產品製造者。在主類中創建並啓動線程,實現向倉庫中添加產品或取走產品。
  如果倉庫類中的store_in() 和store_out()方法不聲明同步,這就是個一般的多線程,我們知道,一個程序中的多線程是交替執行的,運行也是無序的,這樣,就可能存在這樣的問題:
  倉庫中沒有產品了,銷售者還在不斷光顧,而且還不停的在'取'產品,這在現實中是不可思義的,在程序中就表現爲負值;如果將倉庫類中的stroe_in()和store_out()方法聲明同步,如上例所示:就控制了同一時刻只能有一個線程訪問倉庫對象中的同步方法;即一個生產類線程訪問被聲明爲同步的store_in()方法時,其它線程將不能夠訪問對象中的store_out()同步方法,當然也不能訪問store_in()方法。必須等到該線程調用wait()方法放棄鑰匙,其它線程纔有機會訪問同步方法。
  這個原理實際中也很好理解,當生產者(producer)取得倉庫唯一的鑰匙,就向倉庫中添放產品,此時其它的銷售者(customer,可以是一個或多個)不可能取得鑰匙,只有當生產者添放產品結束,交還鑰匙並且通知銷售者,不同的銷售者根據取得鑰匙的先後與否決定是否可以進入倉庫中提走產品。

輕鬆使用線程: 不共享有時是最好的
利用 ThreadLocal 提高可伸縮性
ThreadLocal 類是悄悄地出現在 Java 平臺版本 1.2 中的。雖然支持線程局部變量早就是許多線程工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初設計卻沒有這項有用的功能。而且,最初的實現也相當低效。由於這些原因,ThreadLocal 極少受到關注,但對簡化線程安全併發程序的開發來說,它卻是很方便的。在輕鬆使用線程的第 3 部分,Java 軟件顧問 Brian Goetz 研究了 ThreadLocal 並提供了一些使用技巧。
參加 Brian 的多線程 Java 編程討論論壇以獲得您工程中的線程和併發問題的幫助。
編寫線程安全類是困難的。它不但要求仔細分析在什麼條件可以對變量進行讀寫,而且要求仔細分析其它類能如何使用某個類。有時,要在不影響類的功能、易用性或性能的情況下使類成爲線程安全的是很困難的。有些類保留從一個方法調用到下一個方法調用的狀態信息,要在實踐中使這樣的類成爲線程安全的是困難的。
管理非線程安全類的使用比試圖使類成爲線程安全的要更容易些。非線程安全類通常可以安全地在多線程程序中使用,只要您能確保一個線程所用的類的實例不被其它線程使用。例如,JDBC Connection 類是非線程安全的 ― 兩個線程不能在小粒度級上安全地共享一個 Connection ― 但如果每個線程都有它自己的 Connection,那麼多個線程就可以同時安全地進行數據庫操作。
不使用 ThreadLocal 爲每個線程維護一個單獨的 JDBC 連接(或任何其它對象)當然是可能的;Thread API 給了我們把對象和線程聯繫起來所需的所有工具。而 ThreadLocal 則使我們能更容易地把線程和它的每線程(per-thread)數據成功地聯繫起來。
什麼是線程局部變量(thread-local variable)?
線程局部變量高效地爲每個使用它的線程提供單獨的線程局部變量值的副本。每個線程只能看到與自己相聯繫的值,而不知道別的線程可能正在使用或修改它們自己的副本。一些編譯器(例如 Microsoft Visual C++ 編譯器或 IBM XL FORTRAN 編譯器)用存儲類別修飾符(像 static 或 volatile)把對線程局部變量的支持集成到了其語言中。Java 編譯器對線程局部變量不提供特別的語言支持;相反地,它用 ThreadLocal 類實現這些支持,核心 Thread 類中有這個類的特別支持。
因爲線程局部變量是通過一個類來實現的,而不是作爲 Java 語言本身的一部分,所以 Java 語言線程局部變量的使用語法比內建線程局部變量語言的使用語法要笨拙一些。要創建一個線程局部變量,請實例化類 ThreadLocal 的一個對象。 ThreadLocal 類的行爲與 java.lang.ref 中的各種 Reference 類的行爲很相似;ThreadLocal 類充當存儲或檢索一個值時的間接句柄。清單 1 顯示了 ThreadLocal 接口。
清單 1. ThreadLocal 接口
public class ThreadLocal {
public Object get();
public void set(Object newValue);
public Object initialValue();
}
get() 訪問器檢索變量的當前線程的值;set() 訪問器修改當前線程的值。initialValue() 方法是可選的,如果線程未使用過某個變量,那麼您可以用這個方法來設置這個變量的初始值;它允許延遲初始化。用一個示例實現來說明 ThreadLocal 的工作方式是最好的方法。清單 2 顯示了 ThreadLocal 的一個實現方式。它不是一個特別好的實現(雖然它與最初實現非常相似),所以很可能性能不佳,但它清楚地說明了 ThreadLocal 的工作方式。
清單 2. ThreadLocal 的糟糕實現
public class ThreadLocal {
private Map values = Collections.synchronizedMap(new HashMap());
public Object get() {
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null && !values.containsKey(curThread)) {
o = initialValue();
values.put(curThread, o);
}
return o;
}

public void set(Object newValue) {
values.put(Thread.currentThread(), newValue);
}

public Object initialValue() {
return null;
}
}
這個實現的性能不會很好,因爲每個 get() 和 set() 操作都需要 values 映射表上的同步,而且如果多個線程同時訪問同一個 ThreadLocal,那麼將發生爭用。此外,這個實現也是不切實際的,因爲用 Thread 對象做 values 映射表中的關鍵字將導致無法在線程退出後對 Thread 進行垃圾回收,而且也無法對死線程的 ThreadLocal 的特定於線程的值進行垃圾回收。
用 ThreadLocal 實現每線程 Singleton
線程局部變量常被用來描繪有狀態“單子”(Singleton) 或線程安全的共享對象,或者是通過把不安全的整個變量封裝進 ThreadLocal,或者是通過把對象的特定於線程的狀態封裝進 ThreadLocal。例如,在與數據庫有緊密聯繫的應用程序中,程序的很多方法可能都需要訪問數據庫。在系統的每個方法中都包含一個 Connection 作爲參數是不方便的 ― 用“單子”來訪問連接可能是一個雖然更粗糙,但卻方便得多的技術。然而,多個線程不能安全地共享一個 JDBC Connection。如清單 3 所示,通過使用“單子”中的 ThreadLocal,我們就能讓我們的程序中的任何類容易地獲取每線程 Connection 的一個引用。這樣,我們可以認爲 ThreadLocal 允許我們創建每線程單子。
清單 3. 把一個 JDBC 連接存儲到一個每線程 Singleton 中
public class ConnectionDispenser {
private static class ThreadLocalConnection extends ThreadLocal {
public Object initialValue() {
return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
}
}
private ThreadLocalConnection conn = new ThreadLocalConnection();
public static Connection getConnection() {
return (Connection) conn.get();
}
}
任何創建的花費比使用的花費相對昂貴些的有狀態或非線程安全的對象,例如 JDBC Connection 或正則表達式匹配器,都是可以使用每線程單子(singleton)技術的好地方。當然,在類似這樣的地方,您可以使用其它技術,例如用池,來安全地管理共享訪問。然而,從可伸縮性角度看,即使是用池也存在一些潛在缺陷。因爲池實現必須使用同步,以維護池數據結構的完整性,如果所有線程使用同一個池,那麼在有很多線程頻繁地對池進行訪問的系統中,程序性能將因爭用而降低。
用 ThreadLocal 簡化調試日誌紀錄
其它適合使用 ThreadLocal 但用池卻不能成爲很好的替代技術的應用程序包括存儲或累積每線程上下文信息以備稍後檢索之用這樣的應用程序。例如,假設您想創建一個用於管理多線程應用程序調試信息的工具。您可以用如清單 4 所示的 DebugLogger 類作爲線程局部容器來累積調試信息。在一個工作單元的開頭,您清空容器,而當一個錯誤出現時,您查詢該容器以檢索這個工作單元迄今爲止生成的所有調試信息。
清單 4. 用 ThreadLocal 管理每線程調試日誌
public class DebugLogger {
private static class ThreadLocalList extends ThreadLocal {
public Object initialValue() {
return new ArrayList();
}
public List getList() {
return (List) super.get();
}
}
private ThreadLocalList list = new ThreadLocalList();
private static String[] stringArray = new String[0];
public void clear() {
list.getList().clear();
}
public void put(String text) {
list.getList().add(text);
}
public String[] get() {
return list.getList().toArray(stringArray);
}
}
在您的代碼中,您可以調用 DebugLogger.put() 來保存您的程序正在做什麼的信息,而且,稍後如果有必要(例如發生了一個錯誤),您能夠容易地檢索與某個特定線程相關的調試信息。 與簡單地把所有信息轉儲到一個日誌文件,然後努力找出哪個日誌記錄來自哪個線程(還要擔心線程爭用日誌紀錄對象)相比,這種技術簡便得多,也有效得多。
ThreadLocal 在基於 servlet 的應用程序或工作單元是一個整體請求的任何多線程應用程序服務器中也是很有用的,因爲在處理請求的整個過程中將要用到單個線程。您可以通過前面講述的每線程單子技術用 ThreadLocal 變量來存儲各種每請求(per-request)上下文信息。
ThreadLocal 的線程安全性稍差的堂兄弟,InheritableThreadLocal
ThreadLocal 類有一個親戚,InheritableThreadLocal,它以相似的方式工作,但適用於種類完全不同的應用程序。創建一個線程時如果保存了所有 InheritableThreadLocal 對象的值,那麼這些值也將自動傳遞給子線程。如果一個子線程調用 InheritableThreadLocal 的 get(),那麼它將與它的父線程看到同一個對象。爲保護線程安全性,您應該只對不可變對象(一旦創建,其狀態就永遠不會被改變的對象)使用 InheritableThreadLocal,因爲對象被多個線程共享。InheritableThreadLocal 很合適用於把數據從父線程傳到子線程,例如用戶標識(user id)或事務標識(transaction id),但不能是有狀態對象,例如 JDBC Connection。
ThreadLocal 的性能
雖然線程局部變量早已赫赫有名並被包括 Posix pthreads 規範在內的很多線程框架支持,但最初的 Java 線程設計中卻省略了它,只是在 Java 平臺的版本 1.2 中才添加上去。在很多方面,ThreadLocal 仍在發展之中;在版本 1.3 中它被重寫,版本 1.4 中又重寫了一次,兩次都專門是爲了性能問題。

在 JDK 1.2 中,ThreadLocal 的實現方式與清單 2 中的方式非常相似,除了用同步 WeakHashMap 代替 HashMap 來存儲 values 之外。(以一些額外的性能開銷爲代價,使用 WeakHashMap 解決了無法對 Thread 對象進行垃圾回收的問題。)不用說,ThreadLocal 的性能是相當差的。
Java 平臺版本 1.3 提供的 ThreadLocal 版本已經儘量更好了;它不使用任何同步,從而不存在可伸縮性問題,而且它也不使用弱引用。相反地,人們通過給 Thread 添加一個實例變量(該變量用於保存當前線程的從線程局部變量到它的值的映射的 HashMap)來修改 Thread 類以支持 ThreadLocal。因爲檢索或設置一個線程局部變量的過程不涉及對可能被另一個線程讀寫的數據的讀寫操作,所以您可以不用任何同步就實現 ThreadLocal.get() 和 set()。而且,因爲每線程值的引用被存儲在自已的 Thread 對象中,所以當對 Thread 進行垃圾回收時,也能對該 Thread 的每線程值進行垃圾回收。
不幸的是,即使有了這些改進,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。據我的粗略測量,在雙處理器 Linux 系統上的 Sun 1.3 JDK 中進行 ThreadLocal.get() 操作,所耗費的時間大約是無爭用同步的兩倍。性能這麼差的原因是 Thread.currentThread() 方法的花費非常大,佔了 ThreadLocal.get() 運行時間的三分之二還多。雖然有這些缺點,JDK 1.3 ThreadLocal.get() 仍然比爭用同步快得多,所以如果在任何存在嚴重爭用的地方(可能是有非常多的線程,或者同步塊被頻繁地執行,或者同步塊很大),ThreadLocal 可能仍然要高效得多。
在 Java 平臺的最新版本,即版本 1.4b2 中,ThreadLocal 和 Thread.currentThread() 的性能都有了很大提高。有了這些提高,ThreadLocal 應該比其它技術,如用池,更快。由於它比其它技術更簡單,也更不易出錯,人們最終將發現它是避免線程間出現不希望的交互的有效途徑。
ThreadLocal 的好處
ThreadLocal 能帶來很多好處。它常常是把有狀態類描繪成線程安全的,或者封裝非線程安全類以使它們能夠在多線程環境中安全地使用的最容易的方式。使用 ThreadLocal 使我們可以繞過爲實現線程安全而對何時需要同步進行判斷的複雜過程,而且因爲它不需要任何同步,所以也改善了可伸縮性。除簡單之外,用 ThreadLocal 存儲每線程單子或每線程上下文信息在歸檔方面還有一個頗有價值好處 ― 通過使用 ThreadLocal,存儲在 ThreadLocal 中的對象都是不被線程共享的是清晰的,從而簡化了判斷一個類是否線程安全的工作。
我希望您從這個系列中得到了樂趣,也學到了知識,我也鼓勵您到我的討論論壇中來深入研究多線程問題。

Java多線程學習筆記
一、線程類
  Java是通過Java.lang.Thread類來實現多線程的,第個Thread對象描述了一個單獨的線程。要產生一個線程,有兩種方法:
1、需要從Java.lang.Thread類繼承一個新的線程類,重載它的run()方法;
2、通過Runnalbe接口實現一個從非線程類繼承來類的多線程,重載Runnalbe接口的run()方法。運行一個新的線程,只需要調用它的start()方法即可。如:
/**=====================================================================
* 文件:ThreadDemo_01.java
* 描述:產生一個新的線程
* ======================================================================
*/
class ThreadDemo extends Thread{
Threads()
{
}

Threads(String szName)
{
super(szName);
}

// 重載run函數
public void run()
{
for (int count = 1,row = 1; row < 20; row++,count++)
{
for (int i = 0; i < count; i++)
{
System.out.print('*');
}
System.out.println();
}
}
}

class ThreadMain{
public static void main(String argv[]){
ThreadDemo th = new ThreadDemo();
// 調用start()方法執行一個新的線程
th.start();
}
}

  線程類的一些常用方法:

  sleep(): 強迫一個線程睡眠N毫秒。
  isAlive(): 判斷一個線程是否存活。
  join(): 等待線程終止。
  activeCount(): 程序中活躍的線程數。
  enumerate(): 枚舉程序中的線程。
currentThread(): 得到當前線程。
  isDaemon(): 一個線程是否爲守護線程。
  setDaemon(): 設置一個線程爲守護線程。(用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束)
  setName(): 爲線程設置一個名稱。
  wait(): 強迫一個線程等待。
  notify(): 通知一個線程繼續運行。
  setPriority(): 設置一個線程的優先級。
二、等待一個線程的結束
  有些時候我們需要等待一個線程終止後再運行我們的另一個線程,這時我們應該怎麼辦呢?請看下面的例子:
/**=====================================================================
* 文件:ThreadDemo_02.java
* 描述:等待一個線程的結束
* ======================================================================
*/
class ThreadDemo extends Thread{
Threads()
{
}

Threads(String szName)
{
super(szName);
}

// 重載run函數
public void run()
{
for (int count = 1,row = 1; row < 20; row++,count++)
{
for (int i = 0; i < count; i++)
{
System.out.print('*');
}
System.out.println();
}
}
}

class ThreadMain{
public static void main(String argv[]){
//產生兩個同樣的線程
ThreadDemo th1 = new ThreadDemo();
ThreadDemo th2 = new ThreadDemo();

// 我們的目的是先運行第一個線程,再運行第二個線程
th1.start();
th2.start();
}
}
這裏我們的目標是要先運行第一個線程,等第一個線程終止後再運行第二個線程,而實際運行的結果是如何的呢?實際上我們運行的結果並不是兩個我們想要的直角三角形,而是一些亂七八糟的*號行,有的長,有的短。爲什麼會這樣呢?因爲線程並沒有按照我們的調用順序來執行,而是產生了線程賽跑現象。實際上Java並不能按我們的調用順序來執行線程,這也說明了線程是並行執行的單獨代碼。如果要想得到我們預期的結果,這裏我們就需要判斷第一個線程是否已經終止,如果已經終止,再來調用第二個線程。代碼如下:
/**=====================================================================
* 文件:ThreadDemo_03.java
* 描述:等待一個線程的結束的兩種方法
* ======================================================================
*/
class ThreadDemo extends Thread{
Threads()
{
}

Threads(String szName)
{
super(szName);
}

// 重載run函數
public void run()
{
for (int count = 1,row = 1; row < 20; row++,count++)
{
for (int i = 0; i < count; i++)
{
System.out.print('*');
}
System.out.println();
}
}
}

class ThreadMain{
public static void main(String argv[]){
ThreadMain test = new ThreadMain();
test.Method1();
// test.Method2();
}

// 第一種方法:不斷查詢第一個線程是否已經終止,如果沒有,則讓主線程睡眠一直到它終止爲止
 // 即:while/isAlive/sleep
 public void Method1(){
ThreadDemo th1 = new ThreadDemo();
ThreadDemo th2 = new ThreadDemo();
// 執行第一個線程
th1.start();
// 不斷查詢第一個線程的狀態
while(th1.isAlive()){
try{
Thread.sleep(100);
}catch(InterruptedException e){
}
}
//第一個線程終止,運行第二個線程
th2.start();
}

// 第二種方法:join()
public void Method2(){
ThreadDemo th1 = new ThreadDemo();
ThreadDemo th2 = new ThreadDemo();
// 執行第一個線程
th1.start();
try{
th1.join();
}catch(InterruptedException e){
}
// 執行第二個線程
  th2.start();
}
三、線程的同步問題
有些時候,我們需要很多個線程共享一段代碼,比如一個私有成員或一個類中的靜態成員,但是由於線程賽跑的問題,所以我們得到的常常不是正確的輸出結果,而相反常常是張冠李戴,與我們預期的結果大不一樣。看下面的例子:
/**=============================================================================
* 文件:ThreadDemo_04.java
* 描述:多線程不同步的原因
* =============================================================================
*/
// 共享一個靜態數據對象
class ShareData{
public static String szData = "";
}

class ThreadDemo extends Thread{

private ShareData oShare;

ThreadDemo(){
}

ThreadDemo(String szName,ShareData oShare){
super(szName);
this.oShare = oShare;
}

public void run(){
// 爲了更清楚地看到不正確的結果,這裏放一個大的循環
  for (int i = 0; i < 50; i++){
if (this.getName().equals("Thread1")){
oShare.szData = "這是第 1 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
     try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 輸出結果
System.out.println(this.getName() + ":" + oShare.szData);
}else if (this.getName().equals("Thread2")){
oShare.szData = "這是第 1 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
     try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 輸出結果
System.out.println(this.getName() + ":" + oShare.szData);
}
}
}

class ThreadMain{
public static void main(String argv[]){
ShareData oShare = new ShareData();
ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
ThreadDemo th2 = new ThreadDemo("Thread2",oShare);

th1.start();
th2.start();
}
}
  由於線程的賽跑問題,所以輸出的結果往往是Thread1對應“這是第 2 個線程”,這樣與我們要輸出的結果是不同的。爲了解決這種問題(錯誤),Java爲我們提供了“鎖”的機制來實現線程的同步。鎖的機制要求每個線程在進入共享代碼之前都要取得鎖,否則不能進入,而退出共享代碼之前則釋放該鎖,這樣就防止了幾個或多個線程競爭共享代碼的情況,從而解決了線程的不同步的問題。可以這樣說,在運行共享代碼時則是最多隻有一個線程進入,也就是和我們說的壟斷。鎖機制的實現方法,則是在共享代碼之前加入synchronized段,把共享代碼包含在synchronized段中。上述問題的解決方法爲:

/**=============================================================================
* 文件:ThreadDemo_05.java
* 描述:多線程不同步的解決方法--鎖
* =============================================================================
*/
// 共享一個靜態數據對象
class ShareData{
public static String szData = "";
}

class ThreadDemo extends Thread{

private ShareData oShare;

ThreadDemo(){
}

ThreadDemo(String szName,ShareData oShare){
super(szName);
this.oShare = oShare;
}

public void run(){
// 爲了更清楚地看到不正確的結果,這裏放一個大的循環
  for (int i = 0; i < 50; i++){
if (this.getName().equals("Thread1")){
// 鎖定oShare共享對象
synchronized (oShare){
oShare.szData = "這是第 1 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
      try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 輸出結果
System.out.println(this.getName() + ":" + oShare.szData);
}
}else if (this.getName().equals("Thread2")){
// 鎖定共享對象
synchronized (oShare){
oShare.szData = "這是第 1 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
      try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 輸出結果
System.out.println(this.getName() + ":" + oShare.szData);
}
}
}
}

class ThreadMain{
public static void main(String argv[]){
ShareData oShare = new ShareData();
ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
ThreadDemo th2 = new ThreadDemo("Thread2",oShare);

th1.start();
th2.start();
}
}
  由於過多的synchronized段將會影響程序的運行效率,因此引入了同步方法,同步方法的實現則是將共享代碼單獨寫在一個方法裏,在方法前加上synchronized關鍵字即可。

  在線程同步時的兩個需要注意的問題:
  1、無同步問題:即由於兩個或多個線程在進入共享代碼前,得到了不同的鎖而都進入共享代碼而造成。
  2、死鎖問題:即由於兩個或多個線程都無法得到相應的鎖而造成的兩個線程都等待的現象。這種現象主要是因爲相互嵌套的synchronized代碼段而造成,因此,在程序中儘可能少用嵌套的synchronized代碼段是防止線程死鎖的好方法。

  在寫上面的代碼遇到的一個沒能解決的問題,在這裏拿出來,希望大家討論是什麼原因。

/**=============================================================================
* 文件:ThreadDemo_06.java
* 描述:爲什麼造成線程的不同步。
* =============================================================================
*/
class ThreadDemo extends Thread{
//共享一個靜態數據成員
private static String szShareData = "";

ThreadDemo(){
}

ThreadDemo(String szName){
super(szName);
}

public void run(){
// 爲了更清楚地看到不正確的結果,這裏放一個大的循環
  for (int i = 0; i < 50; i++){
if (this.getName().equals("Thread1")){
     synchronized(szShareData){
szShareData = "這是第 1 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
     try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 輸出結果
System.out.println(this.getName() + ":" + szShareData);
}
}else if (this.getName().equals("Thread2")){
synchronized(szShareData){
szShareData = "這是第 1 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
      try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 輸出結果
System.out.println(this.getName() + ":" + szShareData);
}
}
}
}

class ThreadMain{
public static void main(String argv[]){
ThreadDemo th1 = new ThreadDemo("Thread1");
ThreadDemo th2 = new ThreadDemo("Thread2");

th1.start();
th2.start();
}
}

  這段代碼的共享成員是一個類中的靜態成員,按理說,這裏進入共享代碼時得到的鎖應該是同樣的鎖,而實際上以上程序的輸入卻是不同步的,爲什麼呢??

Java多線程學習筆記(二)
四、Java的等待通知機制
  在有些時候,我們需要在幾個或多個線程中按照一定的秩序來共享一定的資源。例如生產者--消費者的關係,在這一對關係中實際情況總是先有生產者生產了產品後,消費者纔有可能消費;又如在父--子關係中,總是先有父親,然後纔能有兒子。然而在沒有引入等待通知機制前,我們得到的情況卻常常是錯誤的。這裏我引入《用線程獲得強大的功能》一文中的生產者--消費者的例子:
/* ==================================================================================
* 文件:ThreadDemo07.java
* 描述:生產者--消費者
* 注:其中的一些註釋是我根據自己的理解加註的
* ==================================================================================
*/

// 共享的數據對象
class ShareData{
private char c;

public void setShareChar(char c){
this.c = c;
}

public char getShareChar(){
return this.c;
}
}

// 生產者線程
class Producer extends Thread{

private ShareData s;

Producer(ShareData s){
this.s = s;
}

public void run(){
for (char ch = 'A'; ch <= 'Z'; ch++){
try{
Thread.sleep((int)Math.random() * 4000);
}catch(InterruptedException e){}

// 生產
s.setShareChar(ch);
System.out.println(ch + " producer by producer.");
}
}
}

// 消費者線程
class Consumer extends Thread{

private ShareData s;

Consumer(ShareData s){
this.s = s;
}

public void run(){
char ch;

do{
try{
Thread.sleep((int)Math.random() * 4000);
}catch(InterruptedException e){}
// 消費
ch = s.getShareChar();
System.out.println(ch + " consumer by consumer.");
}while(ch != 'Z');
}
}

class Test{
public static void main(String argv[]){
ShareData s = new ShareData();
new Consumer(s).start();
new Producer(s).start();
}
}

  在以上的程序中,模擬了生產者和消費者的關係,生產者在一個循環中不斷生產了從A-Z的共享數據,而消費者則不斷地消費生產者生產的A-Z的共享數據。我們開始已經說過,在這一對關係中,必須先有生產者生產,纔能有消費者消費。但如果運行我們上面這個程序,結果卻出現了在生產者沒有生產之前,消費都就已經開始消費了或者是生產者生產了卻未能被消費者消費這種反常現象。爲了解決這一問題,引入了等待通知(wait/notify)機制如下:
  1、在生產者沒有生產之前,通知消費者等待;在生產者生產之後,馬上通知消費者消費。
  2、在消費者消費了之後,通知生產者已經消費完,需要生產。
下面修改以上的例子(源自《用線程獲得強大的功能》一文):

/* ==================================================================================
* 文件:ThreadDemo08.java
* 描述:生產者--消費者
* 注:其中的一些註釋是我根據自己的理解加註的
* ==================================================================================
*/

class ShareData{

private char c;
// 通知變量
private boolean writeable = true;

// -------------------------------------------------------------------------
// 需要注意的是:在調用wait()方法時,需要把它放到一個同步段裏,否則將會出現
// "java.lang.IllegalMonitorStateException: current thread not owner"的異常。
// -------------------------------------------------------------------------
public synchronized void setShareChar(char c){
if (!writeable){
try{
// 未消費等待
wait();
}catch(InterruptedException e){}
}

this.c = c;
// 標記已經生產
writeable = false;
// 通知消費者已經生產,可以消費
notify();
}

public synchronized char getShareChar(){
if (writeable){
try{
// 未生產等待
wait();
}catch(InterruptedException e){}
}
// 標記已經消費
writeable = true;
// 通知需要生產
notify();
return this.c;
}
}

// 生產者線程
class Producer extends Thread{

private ShareData s;

Producer(ShareData s){
this.s = s;
}

public void run(){
for (char ch = 'A'; ch <= 'Z'; ch++){
try{
Thread.sleep((int)Math.random() * 400);
}catch(InterruptedException e){}

s.setShareChar(ch);
System.out.println(ch + " producer by producer.");
}
}
}

// 消費者線程
class Consumer extends Thread{

private ShareData s;

Consumer(ShareData s){
this.s = s;
}

public void run(){
char ch;

do{
try{
Thread.sleep((int)Math.random() * 400);
}catch(InterruptedException e){}

ch = s.getShareChar();
System.out.println(ch + " consumer by consumer.**");
}while (ch != 'Z');
}
}

class Test{
public static void main(String argv[]){
ShareData s = new ShareData();
new Consumer(s).start();
new Producer(s).start();
}
}

  在以上程序中,設置了一個通知變量,每次在生產者生產和消費者消費之前,都測試通知變量,檢查是否可以生產或消費。最開始設置通知變量爲true,表示還未生產,在這時候,消費者需要消費,於時修改了通知變量,調用notify()發出通知。這時由於生產者得到通知,生產出第一個產品,修改通知變量,向消費者發出通知。這時如果生產者想要繼續生產,但因爲檢測到通知變量爲false,得知消費者還沒有生產,所以調用wait()進入等待狀態。因此,最後的結果,是生產者每生產一個,就通知消費者消費一個;消費者每消費一個,就通知生產者生產一個,所以不會出現未生產就消費或生產過剩的情況。

五、線程的中斷
  在很多時候,我們需要在一個線程中調控另一個線程,這時我們就要用到線程的中斷。用最簡單的話也許可以說它就相當於播放機中的暫停一樣,當第一次按下暫停時,播放器停止播放,再一次按下暫停時,繼續從剛纔暫停的地方開始重新播放。而在Java中,這個暫停按鈕就是Interrupt()方法。在第一次調用interrupt()方法時,線程中斷;當再一次調用interrupt()方法時,線程繼續運行直到終止。這裏依然引用《用線程獲得強大功能》一文中的程序片斷,但爲了更方便看到中斷的過程,我在原程序的基礎上作了些改進,程序如下:

/* ===================================================================================
* 文件:ThreadDemo09.java
* 描述:線程的中斷
* ===================================================================================
*/
class ThreadA extends Thread{

private Thread thdOther;

ThreadA(Thread thdOther){
this.thdOther = thdOther;
}

public void run(){

System.out.println(getName() + " 運行...");

int sleepTime = (int)(Math.random() * 10000);
System.out.println(getName() + " 睡眠 " + sleepTime
+ " 毫秒。");

try{
Thread.sleep(sleepTime);
}catch(InterruptedException e){}

System.out.println(getName() + " 覺醒,即將中斷線程 B。");
// 中斷線程B,線程B暫停運行
thdOther.interrupt();
}
}

class ThreadB extends Thread{
int count = 0;

public void run(){

System.out.println(getName() + " 運行...");

while (!this.isInterrupted()){
System.out.println(getName() + " 運行中 " + count++);

try{
Thread.sleep(10);
}catch(InterruptedException e){
int sleepTime = (int)(Math.random() * 10000);
System.out.println(getName() + " 睡眠" + sleepTime
+ " 毫秒。覺醒後立即運行直到終止。");

try{
Thread.sleep(sleepTime);
}catch(InterruptedException m){}

System.out.println(getName() + " 已經覺醒,運行終止...");
// 重新設置標記,繼續運行
this.interrupt();
}
}

System.out.println(getName() + " 終止。");
}
}

class Test{
public static void main(String argv[]){
ThreadB thdb = new ThreadB();
thdb.setName("ThreadB");

ThreadA thda = new ThreadA(thdb);
thda.setName("ThreadA");

thdb.start();
thda.start();
}
}
  運行以上程序,你可以清楚地看到中斷的過程。首先線程B開始運行,接着運行線程A,在線程A睡眠一段時間覺醒後,調用interrupt()方法中斷線程B,此是可能B正在睡眠,覺醒後掏出一個InterruptedException異常,執行其中的語句,爲了更清楚地看到線程的中斷恢復,我在InterruptedException異常後增加了一次睡眠,當睡眠結束後,線程B調用自身的interrupt()方法恢復中斷,這時測試isInterrupt()返回true,線程退出。
線程和進程(Threads and Processes)
第一個關鍵的系統級概念,究竟什麼是線程或者說究竟什麼是進程?她們其實就是操作系統內部的一種數據結構。
進程數據結構掌握着所有與內存相關的東西:全局地址空間、文件句柄等等諸如此類的東西。當一個進程放棄執行(準確的說是放棄佔有CPU),而被操作系統交換到硬盤上,使別的進程有機會運行的時候,在那個進程裏的所有數據也將被寫到硬盤上,甚至包括整個系統的核心(core memory)。可以這麼說,當你想到進程(process),就應該想到內存(memory) (進程 == 內存)。如上所述,切換進程的代價非常大,總有那麼一大堆的內存要移來移去。你必須用秒這個單位來計量進程切換(上下文切換),對於用戶來說秒意味着明顯的等待和硬盤燈的狂閃(對於作者的我,就意味着IBM龍騰3代的爛掉,5555555)。言歸正傳,對於Java而言,JVM就幾乎相當於一個進程(process),因爲只有進程才能擁有堆內存(heap,也就是我們平時用new操作符,分出來的內存空間)。
那麼線程是什麼呢?你可以把它看成“一段代碼的執行”---- 也就是一系列由JVM執行的二進制指令。這裏面沒有對象(Object)甚至沒有方法(Method)的概念。指令執行的序列可以重疊,並且並行的執行。後面,我會更加詳細的論述這個問題。但是請記住,線程是有序的指令,而不是方法(method)。
線程的數據結構,與進程相反,僅僅只包括執行這些指令的信息。它包含當前的運行上下文(context):如寄存器(register)的內容、當前指令的在運行引擎的指令流中的位置、保存方法(methods)本地參數和變量的運行時堆棧。如果發生線程切換,OS只需把寄存器的值壓進棧,然後把線程包含的數據結構放到某個類是列表(LIST)的地方;把另一個線程的數據從列表中取出,並且用棧裏的值重新設置寄存器。切換線程更加有效率,時間單位是毫秒。對於Java而言,一個線程可以看作是JVM的一個狀態。
運行時堆棧(也就是前面說的存儲本地變量和參數的地方)是線程數據結構一部分。這是因爲多個線程,每一個都有自己的運行時堆棧,也就是說存儲在這裏面的數據是絕對線程安全(後面將會詳細解釋這個概念)的。因爲可以肯定一個線程是無法修改另一個線程的系統級的數據結構的。也可以這麼說一個不訪問堆內存的(只讀寫堆棧內存)方法,是線程安全的(Thread Safe)。
線程安全和同步
線程安全,是指一個方法(method)可以在多線程的環境下安全的有效的訪問進程級的數據(這些數據是與其他線程共享的)。事實上,線程安全是個很難達到的目標。
線程安全的核心概念就是同步,它保證多個線程:
同時開始執行,並行運行
不同時訪問相同的對象實例
不同時執行同一段代碼
我將會在後面的章節,一一細訴這些問題。但現在還是讓我們來看看同步的一種經典的
實現方法——信號量。信號量是任何可以讓兩個線程爲了同步它們的操作而相互通信的對象。Java也是通過信號量來實現線程間通信的。
不要被微軟的文檔所暗示的信號量僅僅是Dijksta提出的計數型信號量所迷惑。信號量其實包含任何可以用來同步的對象。
如果沒有synchronized關鍵字,就無法用JAVA實現信號量,但是僅僅只依靠它也不足夠。我將會在後面爲大家演示一種用Java實現的信號量。
同步的代價很高喲!
同步(或者說信號量,隨你喜歡啦)的一個很讓人頭痛的問題就是代價。考慮一下,下面的代碼:
Listing 1.2:
import java.util.*;
import java.text.NumberFormat;
class Synch
{
private static long[ ] locking_time = new long[100];
private static long[ ] not_locking_time = new long[100];
private static final long ITERATIONS = 10000000;
synchronized long locking (long a, long b){return a + b;}
long not_locking (long a, long b){return a + b;}

private void test( int id )
{
long start = System.currentTimeMillis();
for(long i = ITERATIONS; --i >= 0 ;)
{ locking(i,i); }
locking_time[id] = System.currentTimeMillis() - start;
start = System.currentTimeMillis();
for(long i = ITERATIONS; --i >= 0 ;)
{ not_locking(i,i); }
not_locking_time[id] = System.currentTimeMillis() - start;
}
static void print_results( int id )
{ NumberFormat compositor = NumberFormat.getInstance();
compositor.setMaximumFractionDigits( 2 );
double time_in_synchronization = locking_time[id] - not_locking_time[id];
System.out.println( "Pass " + id + ": Time lost: "
+ compositor.format( time_in_synchronization )
+ " ms. "
+ compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 )
+ "% increase" );
}
static public void main(String[ ] args) throws InterruptedException
{
final Synch tester = new Synch();
tester.test(0); print_results(0);
tester.test(1); print_results(1);
tester.test(2); print_results(2);
tester.test(3); print_results(3);
tester.test(4); print_results(4);
tester.test(5); print_results(5);
tester.test(6); print_results(6);
final Object start_gate = new Object();
Thread t1 = new Thread()
{ public void run()
{ try{ synchronized(start_gate) { start_gate.wait(); } }
catch( InterruptedException e ){}
tester.test(7);
}
};
Thread t2 = new Thread()
{ public void run()
{ try{ synchronized(start_gate) { start_gate.wait(); } }
catch( InterruptedException e ){}
tester.test(8);
}
};
Thread.currentThread().setPriority( Thread.MIN_PRIORITY );
t1.start();
t2.start();
synchronized(start_gate){ start_gate.notifyAll(); }
t1.join();
t2.join();
print_results( 7 );
print_results( 8 );
}
}
這是一個簡單的基準測試程序,她清楚的向大家揭示了同步的代價是多麼的大。test(…)方法調用2個方法1,000,000,0次。其中一個是同步的,另一個則否。下面是在我的機器上輸出的結果(CPU: P4 2.4G(B); Memory: 1GB; OS: windows 2000 server(sp3); JDK: Ver1.4.01 and HotSpot 1.4.01-b01):
C:\>java -verbose:gc Synch
Pass 0: Time lost: 251 ms. 727.5% increase
Pass 1: Time lost: 250 ms. 725% increase
Pass 2: Time lost: 251 ms. 602% increase
Pass 3: Time lost: 250 ms. 725% increase
Pass 4: Time lost: 261 ms. 752.5% increase
Pass 5: Time lost: 260 ms. 750% increase
Pass 6: Time lost: 261 ms. 752.5% increase
Pass 7: Time lost: 1,953 ms. 1,248.82% increase
Pass 8: Time lost: 3,475 ms. 8,787.5% increase
這裏爲了使HotSpot JVM充分的發揮其威力,test( )方法被多次反覆調用。一旦這段程序被徹底優化以後,也就是大約在Pass 6時,同步的代價達到最大。Pass 7 和Pass 8與前面的區別在於,我new了兩個新的線程來並行執行test方法,兩個線程競爭執行(後面是適當的地方,我會解釋什麼是“競爭”,如果你已經等不及了,買本大學的操作系統課本看看吧! J),這使結果更加接近真實。同步的代價是如此之高的,應該儘量避免無謂的同步代價。
現在是時候我們更深入的討論一下同步的代價了。HotSpot JVM一般會使用一到兩個方法來實現同步,這主要取決於是否存在線程的競爭。當沒有競爭的時候,計算機的彙編指令順序的執行,這些指令的執行是不被打斷。指令試圖測試一個比特(bit),然後設置各種二進制位來表示測試的結果,如果這個bit沒有被設置,指令就設置它。這可以說是非常原始的信號量,因爲當兩個線程同步的企圖設置一個bit的值時,只有一個線程可以成功,兩個線程都會檢查結果,看看是不是自己設成功了。
如果bit已經被設置(這裏說的是有線程競爭的情況下),失敗的JVM(線程)不得不離開操作系統的核心進程等待這個bit位被清零。這樣來回的在系統核心中切換是非常耗時的。在NT系統下,需要600次機械指令循環來進入一次系統內核,這還僅僅是進入所耗費的時間還不包括做操作的時間。
是不是覺得很無聊了,呵呵!今天似乎都是些不頂用的東西。但這是必須的,爲了使你能夠讀懂後面的內容。下一篇,我將會談到一些更有趣的話題,例如如何避免同步,如果大家不反對,我還想講一些設計模式的東西。下回見!
避免同步
大部分顯示的同步都可以避免。一般不操作對象狀態信息(例如數據成員)的方法都不需要同步,例如:一些方法只訪問本地變量(也就是說在方法內部聲明的變量),而不操作類級別的數據成員,並且這些方法不會通過傳入的引用參數來修改外部的對象。符合這些條件的方法都不需要使用synchronization這種重量級的操作。除此之外,還可以使用一些設計模式(Design Pattern)來避免同步(我將會在後面提到)。
你甚至可以通過適當的組織你的代碼來避免同步。相對於同步的一個重要的概念就是原子性。一個原子性的操作事不能被其他線程中斷的,通常的原子性操作是不需要同步的。
Java定義一些原子性的操作。一般的給變量付值的操作是原子的,除了long和double。看下面的代碼:
class Unreliable
{
private long x;
public long get_x( ) {return x;}
public void set_x(long value) { x = value; }
}
線程1調用:
obj.set_x( 0 );
線程2調用:
obj.set_x( 0x123456789abcdef )
問題在於下面這行代碼:
x = value;
JVM爲了效率的問題,並沒有把x當作一個64位的長整型數來使用,而是把它分爲兩個32-bit,分別付值:
x.hgh_word = value.high_word;
x.low_word = value.low_word;
因此,存在一個線程設置了高位之後被另一個線程切換出去,而改變了其高位或低位的值。所以,x的值最終可能爲0x0123456789abcdef、0x01234567000000、0x00000000abcdef和0x00000000000000。你根本無法確定它的值,唯一的解決方法是,爲set_x( )和get_x()方法加上synchronized這個關鍵字或者把這個付值操作封裝在一個確保原子性的代碼段裏。
所以,在操作的long型數據的時候,千萬不要想當然。強迫自己記住吧:只有直接付值操作是原子的(除了上面的例子)。其它,任何表達式,象x = ++y、x += y都是不安全的,不管x或y的數據類型是否是小於64位的。很可能在付值之前,自增之後,被其它線程搶先了(preempted)。
競爭條件
在術語中,對於前面我提到的多線程問題——兩個線程同步操作同一個對象,使這個對象的最終狀態不明——叫做競爭條件。競爭條件可以在任何應該由程序員保證原子操作的,而又忘記使用synchronized的地方。在這個意義上,可以把synchronized看作一種保證複雜的、順序一定的操作具有原子性的工具,例如給一個boolean值變量付值,就是一個隱式的同步操作。
不變性
一種有效的語言級的避免同步的方法就是不變性(immutability)。一個自從產生那一刻起就無法再改變的對象就是不變性對象,例如一個String對象。但是要注意類似這樣的表達式:string1 += string2;本質上等同於string1 = string1 + string2;其實第三個包含string1和string2的string對象被隱式的產生,最後,把string1的引用指向第三個string。這樣的操作,並不是原子的。
由於不變對象的值無法發生改變,所以可以爲多個線程安全的同步操作,不需要synchronized。
把一個類的所有數據成員都聲明爲final就可以創建一個不變類型了。那些被聲明爲final的數據成員並不是必須在聲明的時候就寫死,但必須在類的構造函數中,全部明確的初始化。例如:
Class I_am_immutable
{
private final int MAX_VALUE = 10;
priate final int blank_final;
public I_am_immutable( int_initial_value )
{
blank_final = initial_value;
}
}
一個由構造函數進行初始化的final型變量叫做blank final。一般的,如果你頻繁的只讀訪問一個對象,把它聲明成一個不變對象是個保證同步的好辦法,而且可以提高JVM的效率,因爲HotSpot會把它放到堆棧裏以供使用。
同步封裝器(Synchronization Wrappers)
同步還是不同步,
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章