Java多線程(3)

七、線程間通信
    線程間通信,主要介紹問題的引出和如何解決等內容。

1、問題的引出
  實例:
     把一個數據存儲空間劃分爲兩個部分:一部分用於存儲人的姓名,另一部分用於存儲人的性別。這裏包含兩個線程:一個線程向數據存儲空間添加數據(producer),另一個線程從數據存儲空間去除數據(consumer)。這個應用會出現兩種意外。
     第一種意外:假設producer剛向數據存儲空間添加了一個人的姓名,還沒有加入這個人的性別,cpu就切換到consumer線程,consumer則把這個人的姓名和上一個人的性別聯繫到了一起。
     第二種意外:producer放入了若干次數據,consumer纔開始取數據,或者是,consumer 取完一個數據後,還沒等到producer放入新的數據,又重複取出已去過的數據。
  

2、問題如何解決
   程序中的生產者線程和消費者線程運行的是不同的程序代碼,因此編寫包含run方法的兩個類來完成這兩個線程,一個是生產線程Producer,另一個是消費者線程Consumer。
class Producer implements Runnable {
   public void run(){
       //數據空間存放數據
   }
}
class Consumer implements Runnable{
   public void run(){
      //從數據空間讀取數據
   }

還需要定義一個數據結構來存儲數據
class P{
   String name;
   String sex;

範例1:
class P{
   String name = "LiSi";
   String sex = "Girl";
}
class Producer implements Runnable {
   P q = null;
   public Producer(P q){
     this.q = q;
   }
   public void run(){
     int i = 0;
     while(true){
        if(i==0){
          q.name = "ZhangSan";
          q.sex = "Boy";
        }else{
          q.name = "Lisi";
          q.sex = "Girl";
        }
        i = (i+1)%2;
     }
   }
}
class Consumer implements Runnable{
   P q = null;
   public Consumer (P q){
     this.q = q;
   }
   public void run(){
     while(true){
        System.out.println(q.name+"----->"+q.sex);
     }
   }
}
public class Test {
   public static void main(String args[]){
       P q = new P();
       Thread pro = new Thread(new Producer(q));
       Thread con = new Thread(new Consumer(q));
       pro.start();
       con.start();
   }
}

運行結果片段:
......
Lisi----->Girl
ZhangSan----->Boy
ZhangSan----->Boy
Lisi----->Girl
Lisi----->Boy
ZhangSan----->Girl
ZhangSan----->Boy
ZhangSan----->Girl
ZhangSan----->Boy
Lisi----->Boy
Lisi----->Boy
ZhangSan----->Boy
ZhangSan----->Girl
......

   從運行結果來看,打印出現了混亂的情況,這是什麼原因?在程序中,Producer類和Consumer類都操縱了同一個P類,這就是有可能出現Producer類還未操縱玩P類,Consumer類就已經將P類中的內容取走了,這就是資源不同步的原因。爲此,可在P類中增加兩個同步方法:set()和get()。

範例2:
class P{
   String name = "LiSi";
   String sex = "Girl";
   public synchronized void set(String name,String sex){
        this.name = name;
        this.sex = sex;
   }
   public synchronized void get(){
        System.out.println(this.name+"----->"+this.sex);
   }
}
class Producer implements Runnable {
   P q = null;
   public Producer(P q){
     this.q = q;
   }
 
   public void run(){
     int i = 0;
     while(true){
        if(i==0){
          q.set("ZhangSan","BOy");
        }else{
          q.set("LiSi","Girl");
        }
        i = (i+1)%2;
     }
   }
}
class Consumer implements Runnable{
   P q = null;
   public Consumer (P q){
     this.q = q;
   }
   public void run(){
     while(true){
        q.get();
     }
   }
}
public class Test {
   public static void main(String args[]){
       P q = new P();
       Thread pro = new Thread(new Producer(q));
       Thread con = new Thread(new Consumer(q));
       pro.start();
       con.start();
   }
}
運行結果:
........
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
.......
    這個輸出結果順序上沒有任何問題,但又有新的問題產生。Consumer線程對Producer線程放入的一次數據連續的讀取了多次,這並不符合實際的要求。實際要求的結果是,Producer方一次數據,Consumer就取一次;反之,Producher也必須等到Consumer取完後才能放入新的數據,而這一問題的解決就需要使用下面講到的線程間的通信。
    Java是通過Object類的wait、notify、notifyAll這幾個方法來實現線程間的通信的,又因爲所有的類都是從Object繼承的,所以任何類都可以直接使用這些方法。
wait:告訴當前線程放棄監視器並進入睡眠狀態,直到其他線程進入同一監視器並調用notify爲止;
nofity:喚醒同一對象監視器中調用wait的第一個線程。這類似於排隊買票,一個人買完之後,後面的人纔可以繼續買;
notifyAll:喚醒同一對象監視器中調用wait的所有線程,具有最高優先級的線程首先被喚醒並執行。
    我們現在將P類修改如下:
class P{
   String name = "LiSi";
   String sex = "Girl";
   boolean bFull = false;
   public synchronized void set(String name,String sex){
        if(bFull){
           try{
              wait();
           }catch(InterruptedException e){}
        }
        this.name = name;
        try{
           Thread.sleep(10);
        }catch(Exception e){}
        this.sex = sex;
        bFull = true;
        notify();
   }
   public synchronized void get(){
        if(!bFull){
            try{
               wait();
            }catch(InterruptedException e){}
        }
        System.out.println(this.name+"----->"+this.sex);
        bFull = false;
        notify();
   }
}
   當Consumer線程取走數據後,bFull值爲false,當Producer線程放入數據後,bFull值爲true。只有bFull爲true時,Consumer線程才能取走數據,否則就必須等待Producer線程放入新的數據後的通知;反之,只有bFull爲false,Producer線程才能放入新的數據,否則就必須等待Consumer線程取走數據後的通知。
運行結果如下:
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
   wait、notify、notifyAll這3個方法只能在synchronized方法中調用,即無論線程調用一個對象的wait還是notify方法,該線程必須先得到該對象的鎖標記。這樣,notify就只能喚醒同一對象監視器中調用wait的線程。而使用多個對象監視器,就可以分別有多個wait、notify的情況,同組裏的wait只能被同組的notify喚醒。
發佈了35 篇原創文章 · 獲贊 10 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章