1. 程式人生 > 實用技巧 >執行緒協作/通訊

執行緒協作/通訊

什麼是執行緒協作/執行緒通訊

執行緒之間通過某種方式傳遞訊號或訊息,達到互相協作的目的,稱為執行緒通訊/執行緒協作。Java中可以使用object.wait(),object.notify()/object.notify()組合使用或使用JDK1.5之後的Lock介面的方法,作為執行緒通訊的實現。

模版程式碼

等待/通知機制有一套模版程式碼可以直接使用,先看程式碼,後面會解釋模版程式碼為什麼這樣實現

wait()

//用同步塊包裹wait()邏輯
synchronized (monitor) {
    while (!flag) {//當條件不成立時,執行緒進入等待
            wait();
    }
    logic();//當執行緒被喚醒並且條件成立時,執行邏輯程式碼
}

notify()

//用同步塊包裹notify()邏輯
synchronized (this) {
    flag = true;//設定條件成立
    notify();//通知等待的執行緒
}

問題

下面通過幾個問題,來試驗一下wait()/notify()的特性

1. 為什麼wait()/notify()/notifyAll()必須在synchronized同步塊中

保證原子性。

等待/通知的場景中有“條件”這個變數,這個變數的設定操作是在通知執行緒中執行;讀取是在等待執行緒中執行,顯然“設定+通知”和“讀取+等待”必須是原子性的,否則變數的操作和等待通知模式就失去了意義。所以synchronized同步塊的作用是為了保證操作的原子性。

2. 為什麼wait()放在while迴圈裡,放在if裡行不行

不行。

假設使用if:

  1. 第一次條件不滿足時,a執行緒進入等待;
  2. 然後另一個執行緒b執行設定條件為true,並呼叫notify()方法;
  3. 這時a執行緒收到訊號,結束等待,執行logic

如果只有兩個執行緒修改條件變數,那麼if是可行的;但如果2、3步之間,有另一個執行緒將條件變數設定為false,那麼a執行緒結束等待執行logic就是錯誤的。

所以為了確認等待執行緒被喚醒之後,是滿足條件的,必須將條件變數的判斷放在while迴圈中。

3. wait()方法會釋放鎖麼,為什麼

會釋放與該wait()方法所屬物件的內部鎖,其他物件的內部鎖或顯式鎖不會釋放。

呼叫wait()方法的執行緒是等待在某個物件上的,鎖也是作用在某個物件上的。

呼叫wait()方法之後,會釋放該物件上的鎖,以便其他執行緒能獲得鎖,執行notify()方法;如果wait()方法不釋放鎖,其他執行緒嘗試獲取鎖時就會造成死鎖。

4. 等待執行緒被喚醒的時候需要重新獲取鎖麼

需要。

等待執行緒被喚醒後,需要重新持有同步塊的鎖才能進入臨界區繼續執行。如果一直獲取不到鎖,就會一直處於等待狀態,導致的現象是:雖然已經有執行緒修改條件變數並喚醒等待執行緒,但等待執行緒一直沒有執行。

測試用例:https://github.com/Hans-Kl/javaThreads/blob/master/pure/src/signal/synchronize/IsNotifiedNeedLock.java

5. notify()方法會釋放鎖麼

不會。

notify()方法執行完畢不會立即釋放鎖,鎖會在synchronized同步塊執行完成後釋放,所以notify()方法要儘量放在同步塊的最後。防止喚醒了等待執行緒,但等待執行緒又阻塞在了鎖上,導致不必要的上下文切換。

測試用例:https://github.com/Hans-Kl/javaThreads/blob/master/pure/src/signal/synchronize/IsNotifyReleaseLock.java

6. notify()與notifyAll()的區別

假設有三個執行緒都處於等待狀態,喚醒執行緒如果執行一次notify()方法,只有一個執行緒會被喚醒並執行邏輯;喚醒執行緒如果執行一次notifyAll()方法,所有的等待執行緒都會喚醒並競爭鎖資源,沒有得到鎖資源的執行緒會阻塞在這個鎖上等著其他的等待執行緒釋放鎖,最終所有三個等待執行緒都會喚醒並執行邏輯。

測試用例:https://github.com/Hans-Kl/javaThreads/blob/master/pure/src/signal/synchronize/NotifyAndNotifyAll.java

內部實現

monitor物件內部會維護兩個佇列:

  • 入口集Entry Set(鎖池),用於存放申請該物件內部鎖的執行緒;
  • 等待集Wait Set(等待池),用於存放等待在這個物件上的執行緒;

object.wait()方法的虛擬碼實現如下:

package signal.synchronize;

import java.util.Collection;

/**
 * object.wait()方法的虛擬碼實現
 * <p>2020/8/2 18:52</p>
 *
 * @author konglinghan
 * @version 1.0
 */
public class PseudoWait {
    private Collection<Thread> entrySet;//klh 鎖池
    private Collection<Thread> waitSet;//klh 等待池

    public void myWait() {
        //klh wait方法必須在同步塊內持有鎖,否則直接拋異常
        if (!entrySet.contains(Thread.currentThread())) {
            throw new IllegalMonitorStateException();
        }
        //klh 加入等待池
        if (!waitSet.contains(Thread.currentThread())) {
            waitSet.add(Thread.currentThread());
        }
        releaseLock(this);//klh 釋放鎖
        pause(Thread.currentThread());//klh 暫停當前執行緒,等待喚醒<1>

        acquireLock(this);//klh 被喚醒,重新申請鎖資源<2>
        waitSet.remove(Thread.currentThread());//klh 從等待集中移除
        return;//klh wait()方法返回
    }

    /**
     * 暫停執行緒
     *
     * @param thread
     */
    private void pause(Thread thread) {
    }

    /**
     * 申請內部鎖
     *
     * @param monitor 鎖所在的物件
     */
    private void acquireLock(Object monitor) {
        if (entrySet.isEmpty()) {
            return;
        } else {
            //klh 如果沒有獲取到鎖,執行緒阻塞,進入鎖池等待鎖資源
            entrySet.add(Thread.currentThread());
        }
    }

    /**
     * 釋放鎖
     *
     * @param monitor 鎖所在的物件
     */
    private void releaseLock(Object monitor) {
    }

}

客戶程式碼中,執行完object.wait()方法後,執行緒在<1>處暫停;被其他執行緒喚醒後,在<2>處接著執行。可以看到被喚醒之後,第一步就是要重新獲取內部鎖,獲取到鎖資源之後才能繼續執行object.wait()方法。

現在我們重新來看第六個問題:notify()與notifyAll()的區別

notify會將等待池中的一個執行緒喚醒,這個等待執行緒正常的話會直接拿到鎖,從等待集中移出,繼續執行邏輯程式碼;notifyAll()會將等待池中所有的執行緒都喚醒,這些執行緒競爭同一把鎖,沒有競爭到鎖的執行緒進入鎖池阻塞,上一個執行緒釋放鎖後會有另一個執行緒競爭到鎖,並從鎖池中移出,然後從等待池中移出。最終所有被喚醒的執行緒都會從鎖池和等待池中移出,執行邏輯程式碼。