Single Threaded Execution是指“以1個線程執行”的意思。就像細獨木橋只允許一個人通行一樣,這個Pattern用來限制同時只讓一個線程運行。
Single Threaded Execution將會是多線程程序設計的基礎。務必要學好。
Single Threaded Execution有時候也被稱為Critical Section(臨界區)。
Single Threaded Execution是把視點放在運行的線程(過橋的人)上所取的名字,而Critical Section則是把視點放在執行的范圍(橋身)上所取的名字。
范例程序1:不使用Single Threaded Execution Pattern的范例
首先,我們先來看一個應該要使用Single Threaded Execution Pattern而沒有使用和程序范例。這個程序的用意是要實際體驗多線程無法正確執行的程序,會發生什么現象。
模擬3個人頻繁地經過一個只能容許一個人經過的門。當人通過門的時候,這個程序會在計數器中遞增通過的人數。另外,還會記錄通過的人的“姓名與出生地”
表1-1 類一覽表
--------------------------------------------------------------
?名稱?? ??????????????????? 說明
--------------------------------------------------------------
Main?????????? 創建一個門,并操作3個人不斷地穿越門的類
Gate? ???????? 表示門的類,當人經過時會記錄下姓名與出身地
UserThread?????? 表示人的類,只負責處理不斷地在門間穿梭通過
--------------------------------------------------------------
Main類
Main類(List 1-1)用來創建一個門(Gate),并讓3個人(UserThread)不斷通過。創建Gate對象的實例,并將這個實例丟到UserThread類的構造器作為參數,告訴人這個對象“請通過這個門”。
有下面3個人會通過這個門:
Alice - Alaska
Bobby - Brazil
Chris - Canada
為了便于對應兩者之間的關系,筆者在此故意將姓名與出生地設成相同的開頭字母。
在上線程中,先創建3個UserThread類的實例,并以start方法啟動這些線程。
List 1-1 Main.java
--------------------------------------
public class Main {
??? public static void main(String[] args) {
??????? System.out.println("Testing Gate, hit CTRL+C to exit.");
??????? Gate gate = new Gate();
??????? new UserThread(gate, "Alice", "Alaska").start();
??????? new UserThread(gate, "Bobby", "Brazil").start();
??????? new UserThread(gate, "Chris", "Canada").start();
??? }
}
--------------------------------------
并非線程安全的(thread-safe)的Gate類
Gate類(List 1-2)表示人所要通過的門。
counter字段表示目前已經通過這道門的“人數”。name字段表示通過門的行人的“姓名”,而address字段則表示通過者的“出生地”
pass是穿越這道門時使用的方法。在這個方法中,會將表示通過人數的counter字段的值遞增1,并將參數中傳入行人的姓名與出生地,分別拷貝到name字段與address字段中。
this.name = name;
toString方法,會以字符串的形式返回現在門的狀態。使用現在的counter、name、address各字段的值,創建字符串。
check方法,用來檢查現在門的狀態(最后通過的行人的記錄數據)是否正確。當人的姓名(name)與出生地(address)第一個字符不相同時,就斷定記錄是有問題的。當發現記錄有問題時,就顯示出下面的字符串:
****** BROKEN ******
并接著調用toString方法顯示出現在門的狀態。
這個Gate類,在單線程的時候可以正常運行,但是在多線程下就無法正常執行。List 1-2 的Gate類是缺乏安全性的類,并不是線程安全(thread-safe)的類。
List 1-1 非線程安全的Gate類(Gate.java)
??? public class Gate {
??????? private int counter = 0;
??????? private String name = "Nobody";
??????? private String address = "Nowhere";
??????? public void pass(String name, String address) {
??????????? this.counter++;
??????????? this.name = name;
??????????? this.address = address;
??????????? check();
??????? }
??????? public String toString() {
??????????? return "No." + counter + ": " + name + ", " + address;
??????? }
??????? private void check() {
??????????? if (name.charAt(0) != address.charAt(0)) {
??????????????? System.out.println("***** BROKEN ***** " + toString());
??????????? }
??????? }
??? }
UserThread類
UserThread類(List 1-3)表示不斷穿越門的行人。這個類被聲明成Thread類的子類。
List 1-3 UserThread.java
??? public class UserThread extends Thread {
??????? private final Gate gate;
??????? private final String myname;
??????? private final String myaddress;
??????? public UserThread(Gate gate, String myname, String myaddress) {
??????????? this.gate = gate;
??????????? this.myname = myname;
??????????? this.myaddress = myaddress;
??????? }
??????? public void run() {
??????????? System.out.println(myname + " BEGIN");
??????????? while (true) {
??????????????? gate.pass(myname, myaddress);
??????????? }
??????? }
??? }
為什么會出錯呢?
這是因為Gate類的pass方法會被多個線程調用的關系。pass方法是下面4行語句程序代碼所組成:
this.counter++;
this.name = name;
this.address = address;
check();
為了在解說的時候簡單一點,現在只考慮兩個線程(Alice和Bobby)。兩個線程調用pass方法時,上面4行語句可能會是交錯依次執行。如果交錯的情況是圖1-3這樣,那調用check方法的時候,name的值會是“Alice”,而address的值會是“Brazil”。這時就會顯示出 BROKEN了。
圖1-3 線程Alice與線程Bobby調用pass方法時的執行狀況
-----------------------------------------------------------------------------------
線程Alice?????????????? 線程Bobby?????????????? this.name的值??????? this.address的值
-----------------------------------------------------------------------------------
this.counter++???????? this.counter++
?????????????????????????????? this.name = name??????? "Bobby"??
this.name = name?????????????????????????????????????? "Alice"
this.address = address??????????????????????????????? "Alice"???????????? "Alaska"
????????????????????????????? this.address = address? "Alice"???????????? "Brazil"
check()???????????????? ?check()???????????????????????? "Alice"???????????? "Brazil"
****** BROKEN ******
-----------------------------------------------------------------------------------
或者說交錯的情況如圖1-4所示,則調用check方法的時刻,name的值是"Bobby",而address的值會是"Alaska"。這個時候也會顯示出BROKEN。
圖1-4 線程Alice與線程Bobby調用pass方法的執行狀況
------------------------------------------------------------------------------------
線程Alice????????????? 線程Bobby??????????????? this.name的值??????? this.address的值
------------------------------------------------------------------------------------
this.counter++??????? this.counter++?
this.name = name??????????????????????????????????????? "Alice"
???????????????????????????? this.name = name????????? ?"Bobby"
??????????????????????????? ?this.address = address??? "Bobby"????????????? "Brazil"
this.address = address???????????????????????????????? "Bobby"????????????? "Alaska"
check()???????????????? check()?????????????????????????? "Bobby"????????????? "Alaska"
****** BROKEN ******
------------------------------------------------------------------------------------
上述哪一種情況,都使字段name與address的值出現非預期的結果。
通常,線程不會去考慮其他的線程,而自己只會一直不停地跑下去。“線程Alice現在執行到的位置正指定name結束,還沒有指定address的值”,而線程Bobby對此情況并不知情。
范例程序1之所以會顯示出BROKEN,是因為線程并沒有考慮到其他線程,而將共享實例的字段改寫了。
對于name字段來說,有兩個線程在比賽,贏的一方先將值改寫。對address來說,也有兩個線程在比賽誰先將值改寫。像這樣子引發競爭(race)的狀況,我們稱為race condition。有race condition的情況時,就很難預測各字段的值了。
以上是沒有使用Single Threaded Execution Pattern時所發生的現象。
范例程序2:使用Single Threaded Execution Pattern的范例
線程安全的Gate類
List 1-4 是線程安全的Gate類。需要修改的有兩個地方,在pass方法與toString方法前面都加上synchronized。這樣Gate類就成為線程安全的類了。
List 1-4 線程安全的Gate類(Gate.java)
??? public class Gate {
??????? private int counter = 0;
??????? private String name = "Nobody";
??????? private String address = "Nowhere";
??????? public synchronized void pass(String name, String address) {
??????????? this.counter++;
??????????? this.name = name;
??????????? this.address = address;
??????????? check();
??????? }
??????? public synchronized String toString() {
??????????? return "No." + counter + ": " + name + ", " + address;
??????? }
??????? private void check() {
??????????? if (name.charAt(0) != address.charAt(0)) {
??????????????? System.out.println("***** BROKEN ***** " + toString());
??????????? }
??????? }
??? }
synchronized所扮演的角色
如前面一節所說,非線程安全的Gate類之所以會顯示BROKEN, 是因為pass方法內的程序代碼可以被多個線程穿插執行。
synchronized 方法,能夠保證同時只有一個線程可以執行它。這句話的意思是說:線程Alice執行pass方法的時候,線程Bobby就不能調用pass方法。在線程 Alice執行完pass方法之前,線程Bobby會在pass方法的入口處被阻擋下。當線程Alice執行完pass方法之后,將鎖定解除,線程 Bobby才可以開始執行pass方法。
Single Threaded Execution Pattern的所有參與者
SharedResource(共享資源)參與者
Single Threaded Execution Pattern中,有擔任SharedResource角色的類出現。在范例程序2中,Gate類就是這個SharedResource參與者。
SharedResource參與者是可以由多個線程訪問的類。SharedResource會擁有兩類方法:
SafeMethod?? - 從多個線程同時調用也不會發生問題的方法
UnsafeMethod - 從多個線程同時調用會出問題,而需要加以防護的方法。
在Single Threaded Execution Pattern中,我們將UnsafeMethod加以防衛,限制同時只能有一個線程可以調用它。在Java語言中,只要將UnsafeMethod定義成synchronized方法,就可以實現這個目標。
這個必須只讓單線程執行的程序范圍,被稱為臨界區(critical section)
???????????????????????????????????????????????? :SharedResource
---------?????????????????????????????????? -----------------
:Thread? -----------------------|-> synchronized|
---------???????????????????????????????? ?|? UnsafeMethod1|
????????????????????????????????????????????? ?|????????????????????????? ?|
---------????????????????????????????????? |?????????????????????????? |
:Thread? ---------------------->|?? synchronized|
---------?????????????????????????????????? |? UnsafeMethod2|
???????????????????????????????????????????????? -----------------
擴展思考方向的提示
何時使用(適用性)
多線程時
單線程程序,并不需要使用Single Threaded Execution Pattern。因此,也不需要使用到synchronized方法。
數據可以被多個線程訪問的時候
會需要使用Single Threaded Execution Pattern的情況,是在SharedResource的實例可能同時被多個線程訪問的時候。
就算是多線程程序,如果所有線程完全獨立運行,那也沒有使用Single Threaded Execution Pattern的必要。我們將這個狀態稱為線程互不干涉(interfere)。
有些管理多線程的環境,會幫我們確保線程的獨立性,這種情況下這個環境的用戶就不必考慮需不需要使用Single Thread Execution Pattern。
狀態可能變化的時候
當SharedResource參與者狀態可能變化的時候,才會有使用Single Threaded Execution Pattern的需要。
如果實例創建之后,從此不會改變狀態,也沒有用用Single Threaded Execution Pattern的必要。
第二章所要介紹的Immutable Pattern就是這種情況。在Immutable Pattern中,實例的狀態不會改變,所以是不需要用到synchronized方法的一種Pattern。
需要確保安全性的時候
只有需要確保安全性的時候,才會需要使用Single Threaded Execution Pattern。
例如,Java的集合架構類多半并非線程安全。這是為了在不考慮安全性的時候獲得更好的性能。
所以用戶需要考慮自己要用的類需不需要考慮線程安全再使用。
生命性與死鎖
使用Single Threaded Execution Pattern時,可能會發生死鎖(deadlock)的危險。
所謂死鎖,是指兩個線程分別獲取了鎖定,互相等待另一個線程解除鎖定的現象。發生死鎖的時,兩個線程都無法繼續執行下去,所以程序會失去生命性。
舉個例子:
假設Alice與Bobby同吃一個大盤子所盛放的意大利面。盤子的旁邊只有一支湯匙和一支叉子,而要吃意大利面時,需要同時用到湯匙與叉子。
只有一支的湯匙,被Alice拿去了,而只有一支的叉子,去被Bobby拿走了。就造成以下的情況:
握著湯匙的Alice,一直等著Bobby把叉子放下。
握著叉子的Bobby,一直等著Alice的湯匙放下。
這么一來Alice和Bobby只有面面相覷,就這樣不動了。像這樣,多個線程僵持不下,使程序無法繼續運行的狀態,就稱為死鎖。
Single Threaded Execution達到下面這些條件時,可能會出現死鎖的現象。
1.具有多個SharedResource參與者
2.線程鎖定一個SharedResource時,還沒有解除鎖定就前去鎖定另一個SharedResource。
3.線程獲取SharedResource參與者的順序不固定(和SharedResource參與者對等的)。
回過頭來看前面吃不到意大利面的兩個人這個例子。
1.多個SharedResource參與者,相當于湯匙和叉子。
2.鎖定某個SharedResource的參與者后,就去鎖定其他SharedResource。就相當于握著湯匙而想要獲取對方的叉子,或握著叉子而想要獲取對方的湯匙這些操作。
3.SharedResource角色是對等的,就像“拿湯匙->拿叉子”與“拿叉子->拿湯匙”兩個操作都可能發生。也就是說在這里湯匙與叉子并沒有優先級。
1, 2, 3中只要破壞一個條件,就可以避免死鎖的發生。具體的程序代碼如問題1-6
?
問題1-7
某人正思考著若不使用synchronized,有沒有其他的方法可以做到Single Threaded Execution Pattern。而他寫下了如下的Gate類,如代碼1。那么接下來就是問題。請創建Gate類中所要使用的Mutex類。
順帶一提,像Mutex類這種用來進行共享互斥的機制,一般稱為mutex。mutex是mutual exclusion(互斥)的簡稱。
代碼1
??? public class Gate {
??????? private int counter = 0;
??????? private String name = "Nobody";
??????? private String address = "Nowhere";
??????? private final Mutex mutex = new Mutex();
??????? public void pass(String name, String address) { // 并非synchronized
??????????? mutex.lock();
??????????? try {
??????????????? this.counter++;
??????????????? this.name = name;
??????????????? this.address = address;
??????????????? check();
??????????? } finally {
??????????????? mutex.unlock();
??????????? }
??????? }
??????? public String toString() { //? 并非synchronized
??????????? String s = null;
??????????? mutex.lock();
??????????? try {
??????????????? s = "No." + counter + ": " + name + ", " + address;
??????????? } finally {
??????????????? mutex.unlock();
??????????? }
??????????? return s;
??????? }
??????? private void check() {
??????????? if (name.charAt(0) != address.charAt(0)) {
??????????????? System.out.println("***** BROKEN ***** " + toString());
??????????? }
??????? }
??? }
解答范例1:單純的Mutex類
下面是最簡單的 Mutex類,如代碼2。在此使用busy這個boolean類型的字段。busy若是true,就表示執行了lock;如果busy是false,則表示執行了unlock方法。lock與unlock雙方都已是synchronized方法保護著busy字段。
代碼2
??? public final class Mutex {
??????? private boolean busy = false;
??????? public synchronized void lock() {
??????????? while (busy) {
??????????????? try {
??????????????????? wait();
??????????????? } catch (InterruptedException e) {
??????????????? }
??????????? }
??????????? busy = true;
??????? }
??????? public synchronized void unlock() {
??????????? busy = false;
??????????? notifyAll();
??????? }
??? }
代碼2所示的Mutex類在問題1-7會正確地執行。但是,若使用于其他用途,則會發生如下問題。這就是對使用Mutex類的限制。這意味著Mutex類的重復使用性上會有問題。
問題點1
假設有某個線程連續兩次調用lock方法。調用后,在第二次調用時,由于busy字段已經變成true,因此為wait。這就好像自己把自己鎖在外面,進不了門的意思一樣。
問題點2
即使是尚未調用出lock方法的線程,也會變成可以調用unlock方法。就好比即使不是自己上的鎖,自己還是可以將門打開一樣。
解答范例2:改良后的Mutex類
代碼3是將類似范例1中的問題予以改良而形成的新的Mutex類。在此,現在的lock次數記錄在locks字段中。這個lock數是從在lock方法調用的次數扣掉在 unlock方法調用的次數得出的結果。連調用lock方法的線程也記錄在owner字段上。我們現在用locks和owner來解決上述的問題點。
代碼3
??? public final class Mutex {
??????? private long locks = 0;
??????? private Thread owner = null;
??????? public synchronized void lock() {
??????????? Thread me = Thread.currentThread();
??????????? while (locks > 0 && owner != me) {
??????????????? try {
??????????????????? wait();
??????????????? } catch (InterruptedException e) {
??????????????? }
??????????? }
??????????? //assert locks == 0 || owner == me
??????????? owner = me;
??????????? locks++;
??????? }
??????? public synchronized void unlock() {
??????????? Thread me = Thread.currentThread();
??????????? if (locks == 0 || owner != me) {
??????????????? return;
??????????? }
??????????? //assert locks > 0 && owner == me
??????????? locks--;
??????????? if (locks == 0) {
??????????????? owner = null;
??????????????? notifyAll();
??????????? }
??????? }
??? }
測試代碼
代碼4
??? public class UserThread extends Thread {
??????? private final Gate gate;
??????? private final String myname;
??????? private final String myaddress;
??????? public UserThread(Gate gate, String myname, String myaddress) {
??????????? this.gate = gate;
??????????? this.myname = myname;
??????????? this.myaddress = myaddress;
??????? }
??????? public void run() {
??????????? System.out.println(myname + " BEGIN");
??????????? while (true) {
??????????????? gate.pass(myname, myaddress);
??????????? }
??????? }
??? }
代碼5
??? public class Main {
??????? public static void main(String[] args) {
??????????? System.out.println("Testing Gate, hit CTRL+C to exit.");
??????????? Gate gate = new Gate();
??????????? new UserThread(gate, "Alice", "Alaska").start();
??????????? new UserThread(gate, "Bobby", "Brazil").start();
??????????? new UserThread(gate, "Chris", "Canada").start();
??????? }
??? }