閱讀74 返回首頁    go 阿裏雲 go 技術社區[雲棲]


多線程編程 -wait(),notify()/notityAll()方法

多線程編程 -wait(),notify()/notityAll()方法

先說幾點:

一、

注意:這些方法屬於Object,而不屬於Thread。

二、

notify(); //當調用這個方法時,等待隊列裏麵可能沒有等待的線程,那是不是要判斷等待對列是否為空才判斷執不執行呢,沒必要,因為這個耗費不大,就相當於多執行一條語句,所以沒必要判斷,如果要寫判斷語句,那麼代碼還複雜些,所以這個影響不大的情況下,我們沒必要判斷。

三、

每一個對象除了有一個鎖之外,還有一個等待隊列(wait set),當一個對象剛創建的時候,他的等待隊列是空的。

我們可以利用這些方法來解決“生產者和消費者的問題”。

wait方法呢,是在當前線程鎖住對象的鎖後,才調用該對象的wait方法的。即在同步代碼塊中或者同步方法中進行調用的。調用後,該對象的等待隊列中就有了一個所在線程,那個線程進入等待狀態,此時,隻有該對象調用notify方法,才可以把那個線程從隊列裏麵拿出來,使這個線程成為可運行線程。

notifyAll方法就是把該對象等待隊列裏麵的所有線程喚醒,成為可運行線程。


------------------------------------------------------------------------------------------------------------------------------------------


  關於這兩個方法,有很多的內容需要說明.在下麵的說明中可能會有很多地方不能一下子
明白,但在看完本節後,即使不能完全明白,你也一定要回過頭來記住下麵的兩句話:

[wait(),notify()/notityAll()方法是普通對象的方法(Object超類中實現),而不是線程對象的方法]

[wait(),notify()/notityAll()方法隻能在同步方法中調用]

  [線程的互斥控製]
 
  多個線程同時操作某一對象時,一個線程對該對象的操作可能會改變其狀態,而該狀態會影響另一線程對該對象的真正結果.
  這個例子我們在太多的文檔中可以看到,就象兩個售票員同時售出同一張票一樣.

  線程A                           線程B

1.線程A在數據庫中查詢存票,發現票C可以賣出
2.線程A接受用戶訂票請求,準備出票.
3. 這時切換到了線程B執行
4. 線程B在數據庫中查詢存票,發現票C可以賣出                 
5. 線程B將票賣了出去
6.切換到線程A執行,線程A賣了一張已經賣出的票

所以需要一種機製來管理這類問題的發生,當某個線程正在執行一個不可分割的部分時,其它線程不能同時執行這一部分.

像這種控製某一時刻隻能有一個線程執行某個執行單元的機製就叫互斥控製或共享互斥(mutual exclusion)

 在JAVA中,用synchornized關鍵字來實現互斥控製(暫時這樣認為,JDK1.5已經發展了新的機製)

  [synchornized關鍵字]
  把一個單元聲明為synchornized,就可以實現在同一時間隻有一個線程操作該方法.
 
  事實上synchornized就是一把鎖,但是是誰的鎖,鎖誰,這是一個非常複雜的問題.

  每個對象隻有一把監視鎖(monitor lock),一次隻能被一個線程獲取.當一個線程獲取了這一個鎖後,其它線程就隻能等待這個線程釋放該鎖後才能再獲取該鎖.

  那麼synchornized關鍵字到底鎖什麼?得到了誰的鎖?
 
  對於同步塊,synchornized獲取的是參數中的對象的鎖:

  synchornized(obj){
    //...............
  }
  線程執行到這裏時,首先要獲取obj這個實例的鎖,如果沒有獲取到,線程隻能等待.如果多個線程執行到這裏,隻能有一個線程獲取obj的鎖,然後執行{}中的語句,所以,obj對象的作用範圍不同,控製程序不同.
 
  假如:

class Test{

  public void test(){
    Student stu = new Student();
    
    synchornized(stu){
        //...............
    }
  }

  這段程序實現不了同步,沒有實現多線程之間的資源共享。因為每個線程執行到 Student stu = new Student()時會各自產生一個新對象然後獲取這個對象有監視鎖,各自皆大歡喜地執行

  而如果是類的屬性:
  class Test{
    Student stu = new Student();

    public void test(){

        synchornized(stu){
          //...............
        }
    }
  }

  所有執行到Test實例的synchornized(o)的線程,隻有一個線程可以獲取到監視鎖,實現線程的同步,多個線程共享一個stu對象

  有時我們會這樣:

    public void test(){

        synchornized(this){
          //...............
        }
    }
  那麼所有執行Test實例的線程隻能有一個線程執行.而synchornized(o)和
synchornized(this)的範圍是不同的,因為執行到Test實例的synchornized(o)的線程等待時,其它線程可以執行Test實例的synchornized(o1)部分,但多個線程同時隻有一個可以執行Test實例的synchornized(this).
 
  而對於   synchornized(Test.class){
          //...............
    }這樣的同步塊而言,所有調用Test多個實例的線程此時隻能有一個線程可以執行.

  [synchornized方法]

  如果一個方法聲明為synchornized的,則等同於在這個方法上調用synchornized(this).

  如果一個靜態方法被聲明為synchornized,則等同於在這個方法上調用synchornized(類.class).

現在進入wait方法和notify/notifyAll方法.這兩個(或叫三個)方法都是Object對象的方法,而不是線程對象的方法.如同鎖一樣,它們是在線程中調用某一對象上執行的.

  class Test{
    public synchornized void test(){
    //獲取條件,int x 要求大於100;
    
        if(x < 100)
          wait();
    }
  }

  這裏為了說明方法沒有加在try{}catch(){}中,如果沒有明確在哪個對象上調用wait()方法,則為this.wait();
  假如:

    Test t = new Test();
  現在有兩個線程都執行到t.test();方法.其中線程A獲取了t的對象鎖,進入test()方法內.這時x小於100,所以線程A進入等待.

  當一個線程調用了wait方法後,這個線程就進入了這個對象的休息室(waitset),這是一個虛擬的對象,但JVM中一定存在這樣的一個數據結構用來記錄當前對象中有哪些程線程在等待.

  當一個線程進入等待時,它就會釋放鎖,讓其它線程來獲取這個鎖.

  所以線程B有機會獲得了線程A釋放的鎖,進入test()方法,如果這時x還是小於100,線程B也進入了t的休息室.這兩個線程隻能等待其它線程調用notity[All]來喚醒.

  但是如果調用的是有參數的wait(time)方法,則線程A,B都會在休息室中等待這個時間後自動喚醒.


  [為什麼真正的應用都是用while(條件)而不用if(條件)]

在實際的編程中我們看到大量的例子都是用     
        while(x < 100)
          wait();
    go();而不是用if,為什麼呢?
  在多個線程同時執行時,if(x <100)是不安全的.因為如果線程A和線程B都在t的休息室中等待,
這時另一個線程使x==100了,並調用notifyAll方法,線程A繼續執行下麵的go().而它執行完成後,x有可能
又小於100,比如下麵的程序中調用了--x,這時切換到線程B,線程B沒有繼續判斷,直接執行go();就產生一個
錯誤的條件,隻有while才能保證線程B又繼續檢查一次.
 

  [notify/notifyAll方法]
  這兩個方法都是把某個對象在休息區內的線程喚醒,notify隻能喚醒一個,但究竟是哪一個不能確定,而notifyAll則喚醒這個對象上的休息室中所有的線程.


  一般為了安全性,我們在絕對多數時候應該使用notifiAll(),除非你明確知道隻喚醒其中的一個線程.

那麼是否是隻要調用一個對象的wait()方法,當前線程就進入了這個對象的休息室呢?事實中,要調用一個對象的wait()方法,隻有當前線程獲取了這個對象的鎖,換句話說一定要在這個對象的同步方法或以這個對象為參數的同步塊中.
 

class MyThread extends Thread{
Test t = new Test();
  public void run(){
    t.test();
    System.out.println("Thread say:Hello,World!");
  }
}


public class Test {

  int x = 0;
  public void test(){
    if(x==0)
    try{
      wait();
    }catch(Exception e){}
  }
  public static void main(String[] args) throws Exception{
    new MyThread().start();
  }
}

這個線程就不會進入t的wait方法而直接打印出Thread say:Hello,World!.
而如果改成:


public class Test {

  int x = 0;
  public synchornized void test(){
    if(x==0)
    try{
      wait();
    }catch(Exception e){}
  }
  public static void main(String[] args) throws Exception{
    new MyThread().start();
  }
}
我們就可以看到線程一直等待,注意這個線程進入等待後沒有其它線程喚醒,除非強行退出
JVM環境,否則它一直等待.


所以請記住:
1,線程要想調用一個對象的wait()方法就要先獲得該對象的監視鎖,而一旦調用wait()後又立即釋放該鎖

2,如果要把notify/notifyAll和wait方法放在一起用的話,必須先調用notify/notifyAll後調用wait,因為如果調用完wait,該線程就已
經不是current thread了

3,wait(),notify(),notifyAll()方法必須出現在同步方法或同步代碼塊中


最後更新:2017-04-02 22:15:49

  上一篇:go sleep與wait的區別
  下一篇:go 33個地區 iPhone 5 發售,中國不排隊老外很意外