程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java多線程同步問題的探究(四)

Java多線程同步問題的探究(四)

編輯:關於JAVA

四、協作,互斥下的協作——Java多線程協作(wait、notify、notifyAll)

Java監視器支持兩種線程:互斥和協作。

前面我們介紹了采用對象鎖和重入鎖來實現的互斥。這一篇中,我們來看一看線程的協作。

舉個例子:有一家漢堡店舉辦吃漢堡比賽,決賽時有3個顧客來吃,3個廚師來做,一個服務員負責協調漢堡的數量。為了避免浪費,制 作好的漢堡被放進一個能裝有10個漢堡的長條狀容器中,按照先進先出的原則取漢堡。如果容器被裝滿,則廚師停止做漢堡,如果顧客發 現容器內的漢堡吃完了,就可以拍響容器上的鬧鈴,提醒廚師再做幾個漢堡出來。此時服務員過來安撫顧客,讓他等待。而一旦廚師的漢 堡做出來,就會讓服務員通知顧客,漢堡做好了,讓顧客繼續過來取漢堡。

這裡,顧客其實就是我們所說的消費者,而廚師就是生產者。容器是決定廚師行為的監視器,而服務員則負責監視顧客的行為。

在JVM中,此種監視器被稱為等待並喚醒監視器。

在這種監視器中,一個已經持有該監視器的線程,可以通過調用監視對象的wait方法,暫停自身的執行,並釋放監視器,自己進入一個 等待區,直到監視器內的其他線程調用了監視對象的notify方法。當一個線程調用喚醒命令以後,它會持續持有監視器,直到它主動釋放 監視器。而這之後,等待線程會蘇醒,其中的一個會重新獲得監視器,判斷條件狀態,以便決定是否繼續進入等待狀態或者執行監視區域 ,或者退出。

請看下面的代碼:

1.public class NotifyTest {
2.private  String flag = "true";
3.
4.class NotifyThread extends  Thread{
5.public NotifyThread(String name) {
6.super(name);
7.}
8.public void run() {
9.try  {
10.sleep(3000);//推遲3秒鐘通知
11.} catch (InterruptedException e) {
12.e.printStackTrace();
13.}
14.
15.flag = "false";
16.flag.notify();
17.}
18.};
19.
20.class WaitThread extends Thread  {
21.public WaitThread(String name) {
22.super(name);
23.}
24.
25.public void run()  {
26.
27.while (flag!="false") {
28.System.out.println(getName() + " begin waiting!");
29.long  waitTime = System.currentTimeMillis();
30.try {
31.flag.wait();
32.} catch (InterruptedException e)  {
33.e.printStackTrace();
34.}
35.waitTime = System.currentTimeMillis() -  waitTime;
36.System.out.println("wait time :"+waitTime);
37.}
38.System.out.println(getName() + " end  waiting!");
39.
40.}
41.}
42.
43.public static void main(String[] args) throws InterruptedException  {
44.System.out.println("Main Thread Run!");
45.NotifyTest test = new NotifyTest();
46.NotifyThread  notifyThread =test.new NotifyThread("notify01");
47.WaitThread waitThread01 = test.new WaitThread ("waiter01");
48.WaitThread waitThread02 = test.new WaitThread("waiter02");
49.WaitThread waitThread03 =  test.new WaitThread("waiter03");
50.notifyThread.start();
51.waitThread01.start();
52.waitThread02.start ();
53.waitThread03.start();
54.}
55.
56.}

這段代碼啟動了三個簡單的wait線程,當他們處於等待狀態以後,試圖由一個notify線程來喚醒。

運行這段程序,你會發現,滿屏的java.lang.IllegalMonitorStateException,根本不是你想要的結果。

請注意以下幾個事實:

1.任何一個時刻,對象的控制權(monitor)只能被一個線程擁有。

2.無論是執行對象的wait、notify還是notifyAll方法,必須保證當前運行的線程取得了該對象的控制權(monitor)。

3.如果在沒有控制權的線程裡執行對象的以上三種方法,就會報java.lang.IllegalMonitorStateException異常。

4.JVM基於多線程,默認情況下不能保證運行時線程的時序性。

也就是說,當線程在調用某個對象的wait或者notify方法的時候,要先取得該對象的控制權,換句話說,就是進入這個對象的監視器。

通過前面對同步的討論,我們知道,要讓一個線程進入某個對象的監視器,通常有三種方法:

1: 執行對象的某個同步實例方法

2: 執行對象對應的同步靜態方法

3: 執行對該對象加同步鎖的同步塊

顯然,在上面的例程中,我們用第三種方法比較合適。

於是我們將上面的wait和notify方法調用包在同步塊中。

1.synchronized (flag) {
2.flag = "false";
3.flag.notify();
4.}

1.synchronized (flag) {
2.while (flag!="false") {
3.System.out.println(getName() + " begin  waiting!");
4.long waitTime = System.currentTimeMillis();
5.try {
6.flag.wait();
7.} catch  (InterruptedException e) {
8.e.printStackTrace();
9.}
10.waitTime = System.currentTimeMillis() -  waitTime;
11.System.out.println("wait time :"+waitTime);
12.}
13.System.out.println(getName() + " end  waiting!");
14.}

但是,運行這個程序,我們發現事與願違。那個非法監視器異常又出現了。。。

我們注意到,針對flag的同步塊中,我們實際上已經更改了flag對對象的引用: flag="false";

顯然,這樣一來,同步塊也無能為力了,因為我們根本不是針對唯一的一個對象在進行同步。

我們不妨將flag封裝到JavaBean或者數組中去,這樣用JavaBean對象或者數組對象進行同步,就可以達到既能修改裡面參數又不耽誤同 步的目的。

1.private   String flag[] = {"true"}; 

1.synchronized (flag) {
2.flag[0] = "false";
3.flag.notify();
4.}

1.synchronized (flag) {
2.flag[0] = "false";
3.flag.notify();
4.}synchronized (flag) {
5.while  (flag[0]!="false") {
6.System.out.println(getName() + " begin waiting!");
7.long waitTime =  System.currentTimeMillis();
8.try {
9.flag.wait();
10.
11.} catch (InterruptedException e)  {
12.e.printStackTrace();
13.}

運行這個程序,看不到異常了。但是仔細觀察結果,貌似只有一個線程被喚醒。利用jconsole等工具查看線程狀態,發現的確還是有兩 個線程被阻塞的。這是為啥呢?

程序中使用了flag.notify()方法。只能是隨機的喚醒一個線程。我們可以改用flag.notifyAll()方法。這樣,所有被阻塞的線程都會 被喚醒了。

最終代碼請讀者自己修改,這裡不再贅述。

好了,親愛的讀者們,讓我們回到開篇提到的漢堡店大賽問題當中去,來看一看廚師、服務生和顧客是怎麼協作進行這個比賽的。

首先我們構造故事中的三個次要對象:漢堡包、存放漢堡包的容器、服務生

public class Waiter {//服務生,這是個配角,不需要屬性。
}

     class Hamberg {
         //漢堡包
         private int id;//漢堡編號
         private String cookerid;//廚師編號
         public Hamberg(int id, String cookerid){
             this.id = id;
             this.cookerid = cookerid;
             System.out.println(this.toString()+"was made!");
         }
         @Override
         public String toString() {
             return "#"+id+" by "+cookerid;
         }

     }

     class HambergFifo {
         //漢堡包容器
         List<Hamberg> hambergs = new ArrayList<Hamberg>();//借助ArrayList來存放漢堡包
         int maxSize = 10;//指定容器容量
         //放入漢堡
         public <T extends Hamberg> void push(T t) {
             hambergs.add(t);
         }
         //取出漢堡
         public Hamberg pop() {
             Hamberg h = hambergs.get(0);
             hambergs.remove(0);
             return h;
         }
         //判斷容器是否為空
         public boolean isEmpty() {
             return hambergs.isEmpty();
         }
         //判斷容器內漢堡的個數
         public int size() {
             return hambergs.size();
         }
         //返回容器的最大容量
         public int getMaxSize() {
             return this.maxSize;
         }
     }

接下來我們構造廚師對象:

class Cooker implements Runnable {
         //廚師要面對容器
         HambergFifo pool;
         //還要面對服務生
         Waiter waiter;
         public Cooker(Waiter waiter, HambergFifo hambergStack) {
             this.pool = hambergStack;
             this.waiter = waiter;
         }
         //制造漢堡
         public void makeHamberg() {
             //制造的個數
             int madeCount = 0;
             //因為容器滿,被迫等待的次數
             int fullFiredCount = 0;
             try {

                 while (true) {
                     //制作漢堡前的准備工作
                     Thread.sleep(1000);
                     if (pool.size() < pool.getMaxSize()) {
                         synchronized (waiter) {
                             //容器未滿,制作漢堡,並放入容器。
                             pool.push(new Hamberg(++madeCount,Thread.currentThread ().getName()));
                             //說出容器內漢堡數量
                             System.out.println(Thread.currentThread().getName() + ":  There are "
                                           + pool.size() + " Hambergs in  all");
                             //讓服務生通知顧客,有漢堡可以吃了
                             waiter.notifyAll();
                             System.out.println("### Cooker: waiter.notifyAll() :"+
                                       " Hi! Customers, we got some new Hambergs!");
                         }
                     } else {
                         synchronized (pool) {
                             if (fullFiredCount++ < 10) {
                                 //發現容器滿了,停止做漢堡的嘗試。
                                 System.out.println(Thread.currentThread().getName() +  
                                         ": Hamberg Pool is Full, Stop making hamberg");
                                 System.out.println("### Cooker: pool.wait()");
                                 //漢堡容器的狀況使廚師等待
                                 pool.wait();
                             } else {
                                 return;
                             }
                         }
                     }

                     //做完漢堡要進行收尾工作,為下一次的制作做准備。
                     Thread.sleep(1000);
                 }
             } catch (Exception e) {
                 madeCount--;
                 e.printStackTrace();
             }
         }
         public void run() {
             makeHamberg();
         }
     }

接下來,我們構造顧客對象:

class Customer implements Runnable {
         //顧客要面對服務生
         Waiter waiter;
         //也要面對漢堡包容器
         HambergFifo pool;
         //想要記下自己吃了多少漢堡
         int ateCount = 0;
         //吃每個漢堡的時間不盡相同
         long sleeptime;
         //用於產生隨機數
         Random r = new Random();
         public Customer(Waiter waiter, HambergFifo pool) {
             this.waiter = waiter;
             this.pool = pool;
         }
         public void run() {
             while (true) {
                 try {
                     //取漢堡
                     getHamberg();
                     //吃漢堡
                     eatHamberg();
                 } catch (Exception e) {
                     synchronized (waiter) {
                         System.out.println(e.getMessage());
                         //若取不到漢堡,要和服務生打交道
                         try {
                             System.out.println("### Customer: waiter.wait():"+
                                         " Sorry, Sir, there is no hambergs left, please wait!");
                             System.out.println(Thread.currentThread().getName() 
                                         + ": OK, Waiting for new  hambergs");
                             //服務生安撫顧客,讓他等待。
                             waiter.wait();
                             continue;
                         } catch (InterruptedException ex) {
                             ex.printStackTrace();
                         }
                     }
                 }
             }
         }
         private void eatHamberg() {
             try {
                 //吃每個漢堡的時間不等
                 sleeptime = Math.abs(r.nextInt(3000)) * 5;
                 System.out.println(Thread.currentThread().getName() 
                         + ": I'm eating the hamberg for " + sleeptime + "  milliseconds");

                 Thread.sleep(sleeptime);
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
         private void getHamberg() throws Exception {
             Hamberg hamberg = null;
             synchronized (pool) {
                 try {
                     //在容器內取漢堡
                     hamberg = pool.pop();
                     ateCount++;
                     System.out.println(Thread.currentThread().getName() 
                                + ": I Got " + ateCount + " Hamberg " +  hamberg);
                     System.out.println(Thread.currentThread().getName() 
                                 + ": There are still " + pool.size() + "  hambergs left");

                 } catch (Exception e) {
                     pool.notifyAll();
                     System.out.println("### Customer: pool.notifyAll()");
                     throw new Exception(Thread.currentThread().getName() + 
                             ": OH MY GOD!!!! No hambergs left, Waiter![Ring the bell besides the hamberg pool]");
                 }
             }
         }
     }

最後,我們構造漢堡店,讓這個故事發生:

public class HambergShop {
     Waiter waiter = new Waiter();
     HambergFifo hambergPool = new HambergFifo();
     Customer c1 = new Customer(waiter, hambergPool);
     Customer c2 = new Customer(waiter, hambergPool);
     Customer c3 = new Customer(waiter, hambergPool);
     Cooker cooker = new Cooker(waiter, hambergPool);
     public static void main(String[] args) {
         HambergShop hambergShop = new HambergShop();
         Thread t1 = new Thread(hambergShop.c1, "Customer 1");
         Thread t2 = new Thread(hambergShop.c2, "Customer 2");
         Thread t3 = new Thread(hambergShop.c3, "Customer 3");
         Thread t4 = new Thread(hambergShop.cooker, "Cooker 1");
         Thread t5 = new Thread(hambergShop.cooker, "Cooker 2");
         Thread t6 = new Thread(hambergShop.cooker, "Cooker 3");
         t4.start();
         t5.start();
         t6.start();
         try {
             Thread.sleep(10000);
         } catch (Exception e) {
         }
         t1.start();
         t2.start();
         t3.start();
     }
}

運行這個程序吧,然後你會看到我們漢堡店的比賽進行的很好,只是不

知道那些顧客是不是會被撐到。。。

讀到這裡,有的讀者可能會想到前面介紹的重入鎖ReentrantLock。

有的讀者會問:如果我用ReentrantLock來代替上面這些例程當中的 synchronized塊,是不是也可以呢?感興趣的讀者不妨一試。

但是在這裡,我想提前給出結論,就是,

如果用ReentrantLock的lock()和unlock()方法代替上面的synchronized塊,那麼上面這些程序還是要拋出 java.lang.IllegalMonitorStateException異常的,不僅如此,你甚至還會看到線程死鎖。原因就是當某個線程調用第三方對象的wait或 者notify方法的時候,並沒有進入第三方對象的監視器,於是拋出了異常信息。但此時,程序流程如果沒有用finally來處理 unlock方法 ,那麼你的線程已經被lock方法上鎖,並且無法解鎖。程序在java.util.concurrent框架的語義級別死鎖了,你用 JConsole這種工具來檢 測JVM死鎖,還檢測不出來。

正確的做法就是,只使用ReentrantLock,而不使用wait或者notify方法。因為ReentrantLock已經對這種互斥和協作進行了概括。所以 ,根據你程序的需要,請單獨采用重入鎖或者synchronized一種同步機制,最好不要混用。

好了,我們現在明白:

1.線程的等待或者喚醒,並不是讓線程調用自己的wait或者notify方法,而是通過調用線程共享對象的wait或者notify方法來實現。

2.線程要調用某個對象的wait或者notify方法,必須先取得該對象的監視器。

3.線程的協作必須以線程的互斥為前提,這種協作實際上是一種互斥下的協作。

下一講當中,我們來看看如何實實在在的解決線程之間搶占共享資源的問題。敬請期待!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved