Single Threaded Execution是指“以1個(gè)線程執(zhí)行”的意思。就像細(xì)獨(dú)木橋只允許一個(gè)人通行一樣,這個(gè)Pattern用來(lái)限制同時(shí)只讓一個(gè)線程運(yùn)行。
Single Threaded Execution將會(huì)是多線程程序設(shè)計(jì)的基礎(chǔ)。務(wù)必要學(xué)好。
Single Threaded Execution有時(shí)候也被稱為Critical Section(臨界區(qū))。
Single Threaded Execution是把視點(diǎn)放在運(yùn)行的線程(過(guò)橋的人)上所取的名字,而Critical Section則是把視點(diǎn)放在執(zhí)行的范圍(橋身)上所取的名字。
范例程序1:不使用Single Threaded Execution Pattern的范例
首先,我們先來(lái)看一個(gè)應(yīng)該要使用Single Threaded Execution Pattern而沒(méi)有使用和程序范例。這個(gè)程序的用意是要實(shí)際體驗(yàn)多線程無(wú)法正確執(zhí)行的程序,會(huì)發(fā)生什么現(xiàn)象。
模擬3個(gè)人頻繁地經(jīng)過(guò)一個(gè)只能容許一個(gè)人經(jīng)過(guò)的門。當(dāng)人通過(guò)門的時(shí)候,這個(gè)程序會(huì)在計(jì)數(shù)器中遞增通過(guò)的人數(shù)。另外,還會(huì)記錄通過(guò)的人的“姓名與出生地”
表1-1 類一覽表
--------------------------------------------------------------
?名稱?? ??????????????????? 說(shuō)明
--------------------------------------------------------------
Main?????????? 創(chuàng)建一個(gè)門,并操作3個(gè)人不斷地穿越門的類
Gate? ???????? 表示門的類,當(dāng)人經(jīng)過(guò)時(shí)會(huì)記錄下姓名與出身地
UserThread?????? 表示人的類,只負(fù)責(zé)處理不斷地在門間穿梭通過(guò)
--------------------------------------------------------------
Main類
Main類(List 1-1)用來(lái)創(chuàng)建一個(gè)門(Gate),并讓3個(gè)人(UserThread)不斷通過(guò)。創(chuàng)建Gate對(duì)象的實(shí)例,并將這個(gè)實(shí)例丟到UserThread類的構(gòu)造器作為參數(shù),告訴人這個(gè)對(duì)象“請(qǐng)通過(guò)這個(gè)門”。
有下面3個(gè)人會(huì)通過(guò)這個(gè)門:
Alice - Alaska
Bobby - Brazil
Chris - Canada
為了便于對(duì)應(yīng)兩者之間的關(guān)系,筆者在此故意將姓名與出生地設(shè)成相同的開頭字母。
在上線程中,先創(chuàng)建3個(gè)UserThread類的實(shí)例,并以start方法啟動(dòng)這些線程。
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)表示人所要通過(guò)的門。
counter字段表示目前已經(jīng)通過(guò)這道門的“人數(shù)”。name字段表示通過(guò)門的行人的“姓名”,而address字段則表示通過(guò)者的“出生地”
pass是穿越這道門時(shí)使用的方法。在這個(gè)方法中,會(huì)將表示通過(guò)人數(shù)的counter字段的值遞增1,并將參數(shù)中傳入行人的姓名與出生地,分別拷貝到name字段與address字段中。
this.name = name;
toString方法,會(huì)以字符串的形式返回現(xiàn)在門的狀態(tài)。使用現(xiàn)在的counter、name、address各字段的值,創(chuàng)建字符串。
check方法,用來(lái)檢查現(xiàn)在門的狀態(tài)(最后通過(guò)的行人的記錄數(shù)據(jù))是否正確。當(dāng)人的姓名(name)與出生地(address)第一個(gè)字符不相同時(shí),就斷定記錄是有問(wèn)題的。當(dāng)發(fā)現(xiàn)記錄有問(wèn)題時(shí),就顯示出下面的字符串:
****** BROKEN ******
并接著調(diào)用toString方法顯示出現(xiàn)在門的狀態(tài)。
這個(gè)Gate類,在單線程的時(shí)候可以正常運(yùn)行,但是在多線程下就無(wú)法正常執(zhí)行。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)表示不斷穿越門的行人。這個(gè)類被聲明成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);
??????????? }
??????? }
??? }
為什么會(huì)出錯(cuò)呢?
這是因?yàn)镚ate類的pass方法會(huì)被多個(gè)線程調(diào)用的關(guān)系。pass方法是下面4行語(yǔ)句程序代碼所組成:
this.counter++;
this.name = name;
this.address = address;
check();
為了在解說(shuō)的時(shí)候簡(jiǎn)單一點(diǎn),現(xiàn)在只考慮兩個(gè)線程(Alice和Bobby)。兩個(gè)線程調(diào)用pass方法時(shí),上面4行語(yǔ)句可能會(huì)是交錯(cuò)依次執(zhí)行。如果交錯(cuò)的情況是圖1-3這樣,那調(diào)用check方法的時(shí)候,name的值會(huì)是“Alice”,而address的值會(huì)是“Brazil”。這時(shí)就會(huì)顯示出 BROKEN了。
圖1-3 線程Alice與線程Bobby調(diào)用pass方法時(shí)的執(zhí)行狀況
-----------------------------------------------------------------------------------
線程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 ******
-----------------------------------------------------------------------------------
或者說(shuō)交錯(cuò)的情況如圖1-4所示,則調(diào)用check方法的時(shí)刻,name的值是"Bobby",而address的值會(huì)是"Alaska"。這個(gè)時(shí)候也會(huì)顯示出BROKEN。
圖1-4 線程Alice與線程Bobby調(diào)用pass方法的執(zhí)行狀況
------------------------------------------------------------------------------------
線程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的值出現(xiàn)非預(yù)期的結(jié)果。
通常,線程不會(huì)去考慮其他的線程,而自己只會(huì)一直不停地跑下去。“線程Alice現(xiàn)在執(zhí)行到的位置正指定name結(jié)束,還沒(méi)有指定address的值”,而線程Bobby對(duì)此情況并不知情。
范例程序1之所以會(huì)顯示出BROKEN,是因?yàn)榫€程并沒(méi)有考慮到其他線程,而將共享實(shí)例的字段改寫了。
對(duì)于name字段來(lái)說(shuō),有兩個(gè)線程在比賽,贏的一方先將值改寫。對(duì)address來(lái)說(shuō),也有兩個(gè)線程在比賽誰(shuí)先將值改寫。像這樣子引發(fā)競(jìng)爭(zhēng)(race)的狀況,我們稱為race condition。有race condition的情況時(shí),就很難預(yù)測(cè)各字段的值了。
以上是沒(méi)有使用Single Threaded Execution Pattern時(shí)所發(fā)生的現(xiàn)象。
范例程序2:使用Single Threaded Execution Pattern的范例
線程安全的Gate類
List 1-4 是線程安全的Gate類。需要修改的有兩個(gè)地方,在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所扮演的角色
如前面一節(jié)所說(shuō),非線程安全的Gate類之所以會(huì)顯示BROKEN, 是因?yàn)閜ass方法內(nèi)的程序代碼可以被多個(gè)線程穿插執(zhí)行。
synchronized 方法,能夠保證同時(shí)只有一個(gè)線程可以執(zhí)行它。這句話的意思是說(shuō):線程Alice執(zhí)行pass方法的時(shí)候,線程Bobby就不能調(diào)用pass方法。在線程 Alice執(zhí)行完pass方法之前,線程Bobby會(huì)在pass方法的入口處被阻擋下。當(dāng)線程Alice執(zhí)行完pass方法之后,將鎖定解除,線程 Bobby才可以開始執(zhí)行pass方法。
Single Threaded Execution Pattern的所有參與者
SharedResource(共享資源)參與者
Single Threaded Execution Pattern中,有擔(dān)任SharedResource角色的類出現(xiàn)。在范例程序2中,Gate類就是這個(gè)SharedResource參與者。
SharedResource參與者是可以由多個(gè)線程訪問(wèn)的類。SharedResource會(huì)擁有兩類方法:
SafeMethod?? - 從多個(gè)線程同時(shí)調(diào)用也不會(huì)發(fā)生問(wèn)題的方法
UnsafeMethod - 從多個(gè)線程同時(shí)調(diào)用會(huì)出問(wèn)題,而需要加以防護(hù)的方法。
在Single Threaded Execution Pattern中,我們將UnsafeMethod加以防衛(wèi),限制同時(shí)只能有一個(gè)線程可以調(diào)用它。在Java語(yǔ)言中,只要將UnsafeMethod定義成synchronized方法,就可以實(shí)現(xiàn)這個(gè)目標(biāo)。
這個(gè)必須只讓單線程執(zhí)行的程序范圍,被稱為臨界區(qū)(critical section)
???????????????????????????????????????????????? :SharedResource
---------?????????????????????????????????? -----------------
:Thread? -----------------------|-> synchronized|
---------???????????????????????????????? ?|? UnsafeMethod1|
????????????????????????????????????????????? ?|????????????????????????? ?|
---------????????????????????????????????? |?????????????????????????? |
:Thread? ---------------------->|?? synchronized|
---------?????????????????????????????????? |? UnsafeMethod2|
???????????????????????????????????????????????? -----------------
擴(kuò)展思考方向的提示
何時(shí)使用(適用性)
多線程時(shí)
單線程程序,并不需要使用Single Threaded Execution Pattern。因此,也不需要使用到synchronized方法。
數(shù)據(jù)可以被多個(gè)線程訪問(wèn)的時(shí)候
會(huì)需要使用Single Threaded Execution Pattern的情況,是在SharedResource的實(shí)例可能同時(shí)被多個(gè)線程訪問(wèn)的時(shí)候。
就算是多線程程序,如果所有線程完全獨(dú)立運(yùn)行,那也沒(méi)有使用Single Threaded Execution Pattern的必要。我們將這個(gè)狀態(tài)稱為線程互不干涉(interfere)。
有些管理多線程的環(huán)境,會(huì)幫我們確保線程的獨(dú)立性,這種情況下這個(gè)環(huán)境的用戶就不必考慮需不需要使用Single Thread Execution Pattern。
狀態(tài)可能變化的時(shí)候
當(dāng)SharedResource參與者狀態(tài)可能變化的時(shí)候,才會(huì)有使用Single Threaded Execution Pattern的需要。
如果實(shí)例創(chuàng)建之后,從此不會(huì)改變狀態(tài),也沒(méi)有用用Single Threaded Execution Pattern的必要。
第二章所要介紹的Immutable Pattern就是這種情況。在Immutable Pattern中,實(shí)例的狀態(tài)不會(huì)改變,所以是不需要用到synchronized方法的一種Pattern。
需要確保安全性的時(shí)候
只有需要確保安全性的時(shí)候,才會(huì)需要使用Single Threaded Execution Pattern。
例如,Java的集合架構(gòu)類多半并非線程安全。這是為了在不考慮安全性的時(shí)候獲得更好的性能。
所以用戶需要考慮自己要用的類需不需要考慮線程安全再使用。
生命性與死鎖
使用Single Threaded Execution Pattern時(shí),可能會(huì)發(fā)生死鎖(deadlock)的危險(xiǎn)。
所謂死鎖,是指兩個(gè)線程分別獲取了鎖定,互相等待另一個(gè)線程解除鎖定的現(xiàn)象。發(fā)生死鎖的時(shí),兩個(gè)線程都無(wú)法繼續(xù)執(zhí)行下去,所以程序會(huì)失去生命性。
舉個(gè)例子:
假設(shè)Alice與Bobby同吃一個(gè)大盤子所盛放的意大利面。盤子的旁邊只有一支湯匙和一支叉子,而要吃意大利面時(shí),需要同時(shí)用到湯匙與叉子。
只有一支的湯匙,被Alice拿去了,而只有一支的叉子,去被Bobby拿走了。就造成以下的情況:
握著湯匙的Alice,一直等著Bobby把叉子放下。
握著叉子的Bobby,一直等著Alice的湯匙放下。
這么一來(lái)Alice和Bobby只有面面相覷,就這樣不動(dòng)了。像這樣,多個(gè)線程僵持不下,使程序無(wú)法繼續(xù)運(yùn)行的狀態(tài),就稱為死鎖。
Single Threaded Execution達(dá)到下面這些條件時(shí),可能會(huì)出現(xiàn)死鎖的現(xiàn)象。
1.具有多個(gè)SharedResource參與者
2.線程鎖定一個(gè)SharedResource時(shí),還沒(méi)有解除鎖定就前去鎖定另一個(gè)SharedResource。
3.線程獲取SharedResource參與者的順序不固定(和SharedResource參與者對(duì)等的)。
回過(guò)頭來(lái)看前面吃不到意大利面的兩個(gè)人這個(gè)例子。
1.多個(gè)SharedResource參與者,相當(dāng)于湯匙和叉子。
2.鎖定某個(gè)SharedResource的參與者后,就去鎖定其他SharedResource。就相當(dāng)于握著湯匙而想要獲取對(duì)方的叉子,或握著叉子而想要獲取對(duì)方的湯匙這些操作。
3.SharedResource角色是對(duì)等的,就像“拿湯匙->拿叉子”與“拿叉子->拿湯匙”兩個(gè)操作都可能發(fā)生。也就是說(shuō)在這里湯匙與叉子并沒(méi)有優(yōu)先級(jí)。
1, 2, 3中只要破壞一個(gè)條件,就可以避免死鎖的發(fā)生。具體的程序代碼如問(wèn)題1-6
?
問(wèn)題1-7
某人正思考著若不使用synchronized,有沒(méi)有其他的方法可以做到Single Threaded Execution Pattern。而他寫下了如下的Gate類,如代碼1。那么接下來(lái)就是問(wèn)題。請(qǐng)創(chuàng)建Gate類中所要使用的Mutex類。
順帶一提,像Mutex類這種用來(lái)進(jìn)行共享互斥的機(jī)制,一般稱為mutex。mutex是mutual exclusion(互斥)的簡(jiǎn)稱。
代碼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:?jiǎn)渭兊腗utex類
下面是最簡(jiǎn)單的 Mutex類,如代碼2。在此使用busy這個(gè)boolean類型的字段。busy若是true,就表示執(zhí)行了lock;如果busy是false,則表示執(zhí)行了unlock方法。lock與unlock雙方都已是synchronized方法保護(hù)著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類在問(wèn)題1-7會(huì)正確地執(zhí)行。但是,若使用于其他用途,則會(huì)發(fā)生如下問(wèn)題。這就是對(duì)使用Mutex類的限制。這意味著Mutex類的重復(fù)使用性上會(huì)有問(wèn)題。
問(wèn)題點(diǎn)1
假設(shè)有某個(gè)線程連續(xù)兩次調(diào)用lock方法。調(diào)用后,在第二次調(diào)用時(shí),由于busy字段已經(jīng)變成true,因此為wait。這就好像自己把自己鎖在外面,進(jìn)不了門的意思一樣。
問(wèn)題點(diǎn)2
即使是尚未調(diào)用出lock方法的線程,也會(huì)變成可以調(diào)用unlock方法。就好比即使不是自己上的鎖,自己還是可以將門打開一樣。
解答范例2:改良后的Mutex類
代碼3是將類似范例1中的問(wèn)題予以改良而形成的新的Mutex類。在此,現(xiàn)在的lock次數(shù)記錄在locks字段中。這個(gè)lock數(shù)是從在lock方法調(diào)用的次數(shù)扣掉在 unlock方法調(diào)用的次數(shù)得出的結(jié)果。連調(diào)用lock方法的線程也記錄在owner字段上。我們現(xiàn)在用locks和owner來(lái)解決上述的問(wèn)題點(diǎn)。
代碼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();
??????????? }
??????? }
??? }
測(cè)試代碼
代碼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();
??????? }
??? }