wait為什么要在同步塊中使用? 為什么sleep就不用再同步塊中?


 

(1)wait為什么要在同步塊中使用?

  首先wait和notify方法是Object類中的

        

至於為什么它們是放在Object,我們稍后再分析;

 

wait為什么要在同步塊中使用? 

仔細回顧一下,如果wait()方法不在同步塊中,代碼的確會拋出異常:

public class WaitInSyncBlockTest {
 
    @Test
    public void test() {
        try {
            new Object().wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

結果是:

  

 

Lost Wake-Up Problem

事情得從一個多線程編程里面臭名昭著的問題"Lost wake-up problem"說起。

這個問題並不是說只在Java語言中會出現,而是會在所有的多線程環境下出現。

假如有兩個線程,一個消費者線程,一個生產者線程。生產者線程的任務可以簡化成將count加一,而后喚醒消費者;消費者則是將count減一,而后在減到0的時候陷入睡眠:

生產者偽代碼:

count+1;

notify();

消費者偽代碼:

while(count<=0)

   wait()

count--

熟悉多線程的朋友一眼就能夠看出來,這里面有問題。什么問題呢?

生產者是兩個步驟:

  1. count+1;

  2. notify();

消費者也是兩個步驟:

  1. 檢查count值;

  2. 睡眠或者減一;

萬一這些步驟混雜在一起呢?比如說,初始的時候count等於0,這個時候消費者檢查count的值,發現count小於等於0的條件成立;就在這個時候,發生了上下文切換,生產者進來了,噼噼啪啪一頓操作,把兩個步驟都執行完了,也就是發出了通知,准備喚醒一個線程。這個時候消費者剛決定睡覺,還沒睡呢,所以這個通知就會被丟掉。緊接着,消費者就睡過去了……

        

 

這就是所謂的lost wake up問題。

那么怎么解決這個問題呢?

現在我們應該就能夠看到,問題的根源在於,消費者在檢查count到調用wait()之間,count就可能被改掉了。

這就是一種很常見的競態條件。

很自然的想法是,讓消費者和生產者競爭一把鎖,競爭到了的,才能夠修改count的值。

於是生產者的代碼是:

tryLock()
count+1
 
notify()
releaseLock()

消費者的代碼是:

tryLock()
while(count <= 0)
    wait()
 
count-1
releaseLock()

 

Java強制我們的wait()/notify()調用必須要在一個同步塊中,就是不想讓我們在不經意間出現這種lost wake up問題。

不僅僅是這兩個方法,包括java.util.concurrent.locks.Condition的await()/signal()也必須要在同步塊中:

private ReentrantLock lock = new ReentrantLock();
 
private Condition condition = lock.newCondition();
 
@Test
public void test() {
    try {
        condition.signal();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 

准確的來說,即便是我們自己在實現自己的鎖機制的時候,也應該要確保類似於wait()和notify()這種調用,要在同步塊內,防止使用者出現lost wake up問題。 

 

換一個實例分析這個lost wake up問題

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

假設我們要自定義一個blocking queue,如果沒有使用synchronized的話,我們可能會這樣寫:

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // 不能用if,因為為了防止虛假喚醒
            wait();
        return buffer.remove();
    }
}

這段代碼可能會導致如下問題:

  1. 一個消費者調用take,發現buffer.isEmpty
  2. 在消費者調用wait之前,由於cpu的調度,消費者線程被掛起,生產者調用give,然后notify
  3. 然后消費者調用wait (注意,由於錯誤的條件判斷,導致wait調用在notify之后,這是關鍵)
  4. 如果很不幸的話,生產者產生了一條消息后就不再生產消息了,那么消費者就會一直掛起,無法消費,造成死鎖。

解決這個問題的方法就是:總是讓give/notify和take/wait為原子操作。

也就是說wait/notify是線程之間的通信,他們存在競態,我們必須保證在滿足條件的情況下才進行wait。換句話說,如果不加鎖的話,那么wait被調用的時候可能wait的條件已經不滿足了(如上述)。由於錯誤的條件下進行了wait,那么就有可能永遠不會被notify到,所以我們需要強制wait/notify在synchronized中

 

wait與notify原理

  重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的MutexLock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。前面我們在講Java對象頭的時候,講到了monitor這個對象,在hotspot虛擬機中,通過ObjectMonitor類來實現monitor。他的鎖的獲取過程的體現會簡單很多.

     

 

wait 和notify

wait和notify是用來讓線程進入等待狀態以及使得線程喚醒的兩個操作
wait()必須被synchronized來使用,

public class ThreadWait extends Thread{
  private Object lock;
  public ThreadWait(Object lock) {
     this.lock = lock;
  } 

  @Override
  public void run() {
    
      synchronized (lock){
        System.out.println("開始執行 thread wait");
      
        try {
          lock.wait();
         } catch (InterruptedException e) {
          e.printStackTrace();
         } 
       System.out.println("執行結束 thread wait");
      }
   }
  }
public class ThreadNotify(Object lock) {
  this.lock = lock;
} 

@Override
public void run() {
    synchronized (lock){
        System.out.println("開始執行 thread notify");
        lock.notify();
        System.out.println("執行結束 thread notify");
    }
  }
}

 

wait 和notify的原理

  1. 調用wait() 首先會獲取監視器鎖,獲得成功后,會讓線程進入等待狀態進入等待隊列並且釋放鎖;
  2. 然后當其他線程調用notify或者notifyall以后,會選擇從等待隊列中喚醒任意一個線程
  3. 而執行完notify方法以后,並不會立馬喚醒線程,原因是當前線程仍然持有這把鎖,處於等待狀態的線程無法獲得鎖。必須要等到當前的線程執行完按monitorexit指令之后,也就是被釋放之后,處於等待隊列的線程就可以開始競爭鎖了。

     

wait和notify為什么要放在synchronized里面

wait方法的語義有兩個,

  • 釋放當前的對象鎖、
  • 使得當前線程進入阻塞隊列,

而這些操作都和監視器是相關的,所以wait必須要獲得一個監視器鎖。
notify也一樣,它是喚醒一個線程,所以需要知道待喚醒的線程在哪里,就必須找到這個對象獲取這個對象的鎖然后去到這個對象的等待隊列去喚醒一個線程。

 

 

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

為什么sleep就不用再同步塊中?

 

sleep()方法是再Thread類中的

Thread.sleep()的操作的目的是想讓當前線程休息一會,只是暫時不想干活而已,如果這個線程一開始一開始搶到一把鎖,

比如A線程如果先搶到一個鎖,然后B線程因為A線程搶到了就等着。接着A sleep了。B無論如何沒有任何機會去拿到這個鎖。你可以認為這樣就是你預期的,也可以認為這樣實際上因為A的實現,B的執行被卡住,浪費了CPU。因為你當你用了sleep的時候就意味着 你想要讓當前線程不考慮其他線程的感受,只是自己暫時不干活而已。
 
回到問題:為什么sleep就不用再同步塊中?
 
  你把sleep放在同步塊中,但是為什么sleep()需要放在同步塊中呢?放在同步塊中是為了多線程並發處理,大家按照順序依次合理干活,不要造成死鎖飢餓現象,但是sleep()方法就是想讓當前線程進入阻塞狀態,不要干活,我也不釋放我已經擁有的鎖,等到sleep()的時間到了,我再進入就緒態,等待cpu派活給我。sleep根本就不存在多線程並發訪問問題,所以就不需要放在同步塊中。
 
 
 
sleep不釋放鎖 線程是進入阻塞狀態還是就緒狀態?

  答案是進入阻塞狀態,確切的說Thread在Java的狀態TIMED_WAITING(但這個狀態其實並沒那么重要,可以認為是java的內部細節,用戶不用太操心)。往下一層,在不同OS上底層的sleep的實現細節不太一樣。但是大體上就是掛起當前的線程,然后設置一個信號或者時鍾中斷到時候喚醒。sleep后的的Thread在被喚醒前是不會消耗任何CPU的(確切的說,大部分OS都會這么實現,除非某個OS的實現偷懶了)。這點上,wait對當前線程的效果差不多是一樣的,也會暫停調度,等着notify或者一個超時的時間。期間CPU也不會被消耗。
 
關於wait、sleep可以看這篇:   java sleep和wait的區別的疑惑?

 

 

 

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

另一個問題: 為什么wait方法在object類中,sleep方法在Thread類中?

 

wait方法是讓當前線程釋放鎖。然后讓別的線程繼續競爭。阻塞線程

notify通知 喚醒一個阻塞的線程 隨機通知一個

這些都應該屬於資源鎖的動作,而作為鎖,java中鎖一般鎖誰?鎖的是對象,因為對象是資源,是我們需要操作的實體,而Object是所有對象的父類

 

sleep()是讓某個線程暫停運行一段時間,其控制范圍是由當前線程決定,也就是說,在線程里面決定.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM