线程之间的协作
當(dāng)我們使用線程來同時(shí)運(yùn)行多個(gè)任務(wù)時(shí),可以通過使用鎖(互斥)來同步兩個(gè)任務(wù)的行為,從而使得一個(gè)任務(wù)不會(huì)干涉另一個(gè)任務(wù)的資源。也就是說如果兩個(gè)任務(wù)交替著進(jìn)入某項(xiàng)共享資源,你可以使用互斥來使得此時(shí)此刻只有一個(gè)任務(wù)訪問這項(xiàng)資源。有的時(shí)候我們并不是要使線程之間互斥,而是希望它們之間能夠彼此協(xié)作,使得多個(gè)任務(wù)之間可以一起工作去解決問題。
當(dāng)任務(wù)協(xié)作時(shí),關(guān)鍵的問題就是這些任務(wù)之間的握手。為了實(shí)現(xiàn)這種握手,必須要利用線程之間的基礎(chǔ)特性:互斥。在這種情況下,互斥可以確保只有一個(gè)任務(wù)響應(yīng)某個(gè)信號(hào),這樣就可以解決它們之間的競爭關(guān)系。
然而如果只是互斥并不能完成任務(wù)之間的協(xié)作,在互斥的基礎(chǔ)上添加了一種新的途徑,使得它們可以使自身掛起,直至某些外部條件發(fā)生變化,表示這個(gè)任務(wù)是時(shí)候繼續(xù)執(zhí)行了。這種握手可以通過Object 的方法wait() 、notify() 和notifyAll() 來安全的實(shí)現(xiàn)。在Java SE5 并發(fā)庫中還提供了await() 和 signal() 的Condition 對(duì)象。
wait() 與notifyAll()
wait() 會(huì)在等待外部條件改變時(shí)將任務(wù)掛起,并且只有在notify() 與notifyAll() 發(fā)生時(shí),這個(gè)任務(wù)就會(huì)被喚醒去檢查所發(fā)生的變化。因此wait() 提供了一種在任務(wù)之間對(duì)活動(dòng)進(jìn)行同步的方式。
這里需要注意的是:當(dāng)我們調(diào)用sleep() 的時(shí)候鎖并沒有被釋放,調(diào)用yield() 方法時(shí)也是這種情況,這一點(diǎn)很重要,當(dāng)一個(gè)任務(wù)中調(diào)用了wait() 的時(shí)候,線程的執(zhí)行將會(huì)被掛起,該對(duì)象上的鎖將被釋放,也就是說wait() 操作會(huì)釋放鎖。這意味著其他的任務(wù)將會(huì)獲得這個(gè)鎖,去執(zhí)行它的任務(wù)。
wait() 有兩種方式,第一種接收毫秒數(shù)作為參數(shù),含義與sleep() 方法類似,都是指在制定的時(shí)間內(nèi)“暫停執(zhí)行”。但是與sleep() 不同的是對(duì)于wait() 而言:
[1]:在執(zhí)行wait() 前進(jìn)鎖是釋放的
[2]:你可以通過notify() 與notifyAll() ,或者時(shí)間到期,可以從wait() 中恢復(fù)過來
另一種方式是wait() 不接受任何的參數(shù)。這種等待將會(huì)一直持續(xù)下去直至接收到notify() 與notifyAll() 的消息。
其中wait() 、notify() 與notifyAll() 有一個(gè)特殊的地方就是:它們并不是Thread 的一部分,這些方法屬于基類Object。這么做是有道理的(一會(huì)你就可以體會(huì)到這么做的好處),因?yàn)檫@些方法操作的鎖是所有對(duì)象的一部分,你可以把wait() 放在任何同步控制的方法里,而不需要考慮這個(gè)類是否繼承了Thread 類或者實(shí)現(xiàn)了 Runnable 接口。
notify() 與notifyAll()
使用notify() 時(shí),是指眾多等待同一個(gè)鎖的任務(wù)中只有一個(gè)被喚醒,如果你使用notify() 那么必須要保證被喚醒的是恰當(dāng)?shù)娜蝿?wù)。另一方面為了使用notify() ,所有的任務(wù)都必須等待相同的條件,如果你有多個(gè)任務(wù)在等待多個(gè)不同的條件,那么你就不知道是否喚醒了恰當(dāng)?shù)娜蝿?wù)。notifyAll() 會(huì)喚醒所有等待這個(gè)鎖的任務(wù)
案例
在這里用三個(gè)具體的案例去實(shí)現(xiàn)線程之間的相互協(xié)作,第一個(gè)案例就是我們?cè)趍ain 方法中實(shí)現(xiàn)子方法執(zhí)行輸出三次后主方法程執(zhí)行輸出三次,總共循環(huán)三次這樣的步驟。在main() 方法中有兩個(gè)線程執(zhí)行,它們之間共同使用model 對(duì)象這把鎖。
public class MainThread {public static void main(String[] args) {ThreadModel model = new ThreadModel();new Thread(new Runnable() {@Overridepublic void run() {for(int i=0;i<3;i++)model.subMethod(i+1);}}){}.start();for(int i=0;i<3;i++){model.mainMethod(i+1);}} } class ThreadModel{private boolean b = true; //檢查標(biāo)志public synchronized void subMethod(int count){while (!b){ //當(dāng)b 為 false 的時(shí)候子方法掛起,并釋放鎖,此時(shí)主方法將會(huì)獲得該鎖try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//b 為true 的時(shí)候子方法獲得鎖后開始進(jìn)行輸出,將標(biāo)志置為false 并將主方法的任務(wù)喚醒for(int i=0;i<3;i++){System.out.println("子線程執(zhí)行第"+(i+1)+"次"+"...."+count);}b = false;this.notify();}public synchronized void mainMethod(int count){while (b){ //當(dāng)b 為true 的時(shí)候主方法將掛起,釋放鎖,此時(shí)子方法獲得該鎖try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//b 為false 的時(shí)候主方法獲得鎖后開始進(jìn)行輸出,將標(biāo)志置為true 并將子方法的任務(wù)喚醒for(int i = 0;i <3;i++){System.out.println("主線程執(zhí)行第"+(i+1)+"次"+"...."+count);}b = true; this.notify();} }輸出:
子線程執(zhí)行第1次….1
子線程執(zhí)行第2次….1
子線程執(zhí)行第3次….1
主線程執(zhí)行第1次….1
主線程執(zhí)行第2次….1
主線程執(zhí)行第3次….1
子線程執(zhí)行第1次….2
子線程執(zhí)行第2次….2
子線程執(zhí)行第3次….2
主線程執(zhí)行第1次….2
主線程執(zhí)行第2次….2
主線程執(zhí)行第3次….2
子線程執(zhí)行第1次….3
子線程執(zhí)行第2次….3
子線程執(zhí)行第3次….3
主線程執(zhí)行第1次….3
主線程執(zhí)行第2次….3
主線程執(zhí)行第3次….3
下面這個(gè)例子使用線程池演示了兩個(gè)過程:一個(gè)是將蠟涂到Car 上,一個(gè)是拋光它。拋光任務(wù)在涂蠟任務(wù)完成之前,是不能執(zhí)行工作的,涂蠟任務(wù)在涂另一層蠟之前必須等待拋光任務(wù)完成。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;class Car{private boolean waxOn = false;public synchronized void waxed(){ //打蠟waxOn = true;notifyAll();}public synchronized void buffed() { //拋光waxOn = false;notifyAll();}public synchronized void waitForWaxing() throws InterruptedException {while (!waxOn)wait();}public synchronized void waitForBuffing() throws InterruptedException {while (waxOn)wait();} }class WaxOn implements Runnable{private Car car;public WaxOn(Car car){this.car = car;}@Overridepublic void run() {try{while (true){System.out.print("Wax On!"); //開始打蠟Thread.sleep(200);car.waxed(); //將標(biāo)志置為true ,喚醒正在等待的WaxOff中 的任務(wù)car.waitForBuffing(); //此時(shí)打蠟完成后將掛起等待拋光后被喚醒}} catch (InterruptedException e) {System.out.println("Exiting via interrupt");}System.out.println("Ending Wax On task");} }class WaxOff implements Runnable{private Car car;public WaxOff(Car car){this.car = car;}@Overridepublic void run() {try{while (true){car.waitForWaxing(); //如果waxOn 為false 時(shí)掛起,釋放鎖,下面任務(wù)不執(zhí)行,當(dāng)waxOn時(shí)被喚醒System.out.print("Wax Off!"); //此時(shí)打蠟任務(wù)完成Thread.sleep(200);car.buffed(); //將waxOn 置為false 喚醒WaxOn 中的任務(wù)}} catch (InterruptedException e) {System.out.println("Exiting via interrupt");}System.out.println("Ending Wax Off task");} }public class WaxOMatic {public static void main(String[] args) throws InterruptedException {Car car = new Car();ExecutorService exec = Executors.newCachedThreadPool();exec.execute(new WaxOff(car));exec.execute(new WaxOn(car));Thread.sleep(1000); exec.shutdownNow(); //1s 之后終止這兩個(gè)線程,調(diào)用exec.shutdownNow()時(shí)會(huì)調(diào)用控制線程的interrupt()} }輸出
Wax On!Wax Off!Wax On!Wax Off!Wax On!Exiting via interrupt
Ending Wax Off task
Exiting via interrupt
Ending Wax On task
下面這個(gè)示例是一個(gè)生產(chǎn)者與消費(fèi)者的問題,只有在生產(chǎn)完成后才可以被消費(fèi),當(dāng)消費(fèi)完成后再執(zhí)行生產(chǎn)任務(wù),并且每次只生產(chǎn)與消費(fèi)一個(gè)資源。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;class ReSource{private String name;private int count = 1;private boolean flag = false;public synchronized void set(String name){while(true){while(flag){ //循環(huán)判斷標(biāo)記,被喚醒后仍要判斷標(biāo)記,避免兩個(gè)生產(chǎn)者任務(wù)掛起時(shí)產(chǎn)生數(shù)據(jù)錯(cuò)亂try {wait();} catch (InterruptedException e) {System.out.println("生產(chǎn)完成");}}this.name = name + count;count++;System.out.println(Thread.currentThread().getName()+ "...生產(chǎn)者...." + this.name);flag = true;notifyAll(); //喚醒所有的線程,意味著消費(fèi)者的線程也被喚醒,避免所有線程都處于等待狀態(tài)}}public synchronized void out(){while(true){while(!flag){try {wait();} catch (InterruptedException e) {System.out.println("消費(fèi)完成");}}System.out.println(Thread.currentThread().getName()+"...消費(fèi)者...." + this.name);flag = false;notifyAll();}} }class Producer implements Runnable{ReSource rs;Producer(ReSource rs){this.rs = rs;}public void run(){rs.set("烤鴨");} }class Consumer implements Runnable{ReSource rs;Consumer(ReSource rs){this.rs = rs;}public void run(){rs.out();}}public class Restaurant {public static void main(String args []) throws InterruptedException {ReSource rs = new ReSource();Producer producer = new Producer(rs); //多個(gè)生產(chǎn)者與消費(fèi)者的案例Consumer consumer = new Consumer(rs);ExecutorService service = Executors.newCachedThreadPool();service.execute(new Thread(producer));service.execute(new Thread(producer));service.execute(new Thread(consumer));service.execute(new Thread(consumer));Thread.sleep(10);service.shutdownNow();System.exit(0);} }輸出
pool-1-thread-1…生產(chǎn)者….烤鴨1
pool-1-thread-4…消費(fèi)者….烤鴨1
pool-1-thread-2…生產(chǎn)者….烤鴨2
pool-1-thread-4…消費(fèi)者….烤鴨2
pool-1-thread-1…生產(chǎn)者….烤鴨3
pool-1-thread-4…消費(fèi)者….烤鴨3
pool-1-thread-2…生產(chǎn)者….烤鴨4
pool-1-thread-4…消費(fèi)者….烤鴨4
……………………….
上面三個(gè)示例中在判斷標(biāo)志讓線程掛起的時(shí)候我們都是使用了while() 循環(huán),使用while() 循環(huán)的好處有很多:
- 你可能有多個(gè)任務(wù)出于不同的原因在等待同一個(gè)鎖,而第一個(gè)喚醒任務(wù)可能會(huì)改變這種狀況(即使你沒有這么做,但是可能有人繼承你的類去這么做)。如果出現(xiàn)這種情況,那么這個(gè)任務(wù)應(yīng)該被再次掛起,直至其感興趣的條件發(fā)生變化。
- 在這個(gè)任務(wù)從其wait() 被喚醒的時(shí)刻,有可能會(huì)在某個(gè)其他的任務(wù)中做出了改變,從而使得這個(gè)任務(wù)不能執(zhí)行,或者執(zhí)行其他的操作顯得無關(guān)緊要。此時(shí)應(yīng)該再次調(diào)用wait() 將其掛起。
- 也有可能某些任務(wù)出于不同的原因在等待你對(duì)象上的鎖。在這種情況下,你需要檢查是否已經(jīng)由正確的原因喚醒,如果不是就再次掛起。
在示例三中如果不用while() 循環(huán)判斷標(biāo)記,當(dāng)兩個(gè)生產(chǎn)者線程都處于掛起狀態(tài)時(shí),如果一個(gè)消費(fèi)者此時(shí)完成了消費(fèi)任務(wù),那么這兩個(gè)生產(chǎn)者 都會(huì)被喚醒,如果一個(gè)生產(chǎn)者得到了資源并完成了生產(chǎn)任務(wù),此時(shí)另一個(gè)生產(chǎn)者線程得到了執(zhí)行權(quán),在if() 條件下由于不對(duì)標(biāo)記進(jìn)行判斷另一個(gè)生產(chǎn)者在原來掛起的基礎(chǔ)上得到執(zhí)行權(quán)它也會(huì)去執(zhí)行生成的任務(wù),但是它本來是應(yīng)該被掛起的。此時(shí)就會(huì)出現(xiàn)問題,while() 循環(huán)則不會(huì),在線程被喚醒后它還會(huì)循環(huán)區(qū)判斷標(biāo)記,這時(shí)候的標(biāo)記將會(huì)引導(dǎo)它執(zhí)行還是再度掛起。
參考書籍:
《Java 編程思想》Bruce Eckel 著 陳昊鵬 譯
總結(jié)
- 上一篇: 共达电声是国企吗
- 下一篇: 为什么 HashMap 常用 Strin