条件队列大法好:wait和notify的基本语义
條件隊列是我們常用的輕量級同步機制,也被稱為“wait+notify”機制。但很多剛剛接觸并發的朋友可能會對wait和notify的語義和配合過程感到迷惑。
今天從join()方法的實現切入,重點講解wait()方法的語義,簡略提及notify()與notifyAll()的語義,最后總結二者的配合過程。
本篇的知識點很淺,但牢固掌握很重要。后面會再寫一篇文章,介紹wait+nofity的用法,和使用時的一些問題。
基本概念
線程、Thread與Object
在理解“wait+notify”機制時,注意區分線程、Thread與Object的概念,明確三者在wait、 notify、鎖競爭等事件中充當的角色:
- 線程指操作系統中的線程
- Thread指Java中的線程類
- Object指Java中的對象
Thread繼承自Object,也是一個對象(多態),并從Object類中繼承得到了wait()、notify()(還有notifyAll())方法;同時,Thread也被JVM用于映射操作系統中的線程。
wait()
迷惑的join()方法
通過join()方法確認你是否理解了wait+notify機制:
Thread f = new Thread(new Runnable() {public run() {Thread s = new Thread(new Runnable() {public run() {for (int i : 1000000) {sout(i);}}});s.start();sout("************* son thread started *************");s.join();sout("************* son thread died *************");} }); f.start(); 復制代碼join()方法的語義很簡單,可以不嚴謹的表述為“讓父線程等待子線程退出”。現在我們來觀察Thread#join()的實現,讓你對這個語義產生迷惑:
public final synchronized void join(long millis) throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) {wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}wait(delay);now = System.currentTimeMillis() - base;}} } 復制代碼重點看15-22行。邏輯很簡單,一個限時阻塞的經典寫法。不過,你可能會產生和我一樣的迷惑:
為什么調用子線程的wait()方法,進入等待狀態的卻是父線程呢?
分析
讓我們用前面提到的線程、Thread和Object三個概念來解釋這段代碼。事件序列如下:
- 主線程t0執行18行后,操作系統中創建了線程t1,Thread實例f轉入RUNNABLE狀態(Java中,Thread沒有RUN狀態,因為線程是否正在執行由JVM之外的調度策略決定)
- 假設線程t1正在執行,則線程t1執行4-11行,在Java中創建了Thread實例s,處于NEW狀態;同時,s也是一個Object實例
- 線程t1執行12行后,操作系統中創建了線程t2,Thread實例s轉入RUNNABLE狀態
- 假設線程t1、t2均正在執行,則線程t1執行12行之后、14行之前,可能線程t1與線程t2同時在向標準輸出打印內容(t1執行13行,t2執行7-9行)
- 線程t1執行14行的過程中,操作系統中的線程t1轉入阻塞或等待狀態(取決于操作系統的實現),Thread實例f轉入TIMED_WAITING狀態,Thread實例s不受影響,仍處于RUNNABLE狀態
- 線程t2死亡后,被操作系統標記為死亡,Thread實例s轉入為TERMINATED狀態
- 線程t1中,Thread實例f發現Thread實例s不再存活,隨即轉入RUNNABLE狀態,操作系統中的線程t1轉入運行狀態
- 線程t1從14行s.join()返回,執行15行,打印
- 最后,線程t1死亡,Thread實例也轉入了TERMINATED狀態
當然,在事件6(線程t1執行14行的過程中),Thread實例f在TIMED_WAITING狀態與RUNNABLE狀態之間來回轉換,也因此,才能發現Thread實例s不再存活。但可忽略RUNNABLE狀態,不影響理解。
上一節提出的問題忽略了線程、Thread與Object的區別?,F在,耐心分析過事件序列之后,讓我們使用這三個概念,重新表述該問題:
為什么在父線程t1中調用s.join(),進而調用s.wait(),進入等待狀態的卻是Thread實例f對應的父線程t1,而不是子線程t2呢?
該表述同時也是回答。因為wait()影響的是調用wait()的線程,而不是wait()所屬的Object實例。具體說,wait()的語義是“將調用s.wait()的線程t1放入Object實例s的等待集合”。這與s是否同時是Thread實例并無關系——如果s恰好是一個Thread實例,那么其所對應的線程t2可以照常運行,毫無影響。
雖然線程的狀態與Thread實例的狀態不能一一對應,但用Thread實例的狀態代替線程的狀態,可以簡化條件隊列的模型,又不影響核心的正確性。在事件6(線程t1執行14行的過程中)中,各角色的關系如圖:
更容易理解的用法
我們之所以會在join()方法的實現上產生困惑,是因為它以一種難以理解的姿勢使用wait+notify機制。
wait+notify機制本質上是一種基于條件隊列的同步。JVM為每個對象都內置了監視器,與java.util.concurrent包中的條件隊列Condition對應。
條件隊列本身很容易理解,但join()方法使用wait()的姿勢讓人迷惑。它將Thread實例s作為條件隊列,共享于父線程t1、子線程t2中——Thread實例s既能夠被創建它的Thread實例f訪問,也能夠被它自己(this)訪問。可讀性很差,不建議學習。
那么,如何使用wait()才更容易理解呢?可參考Java實現生產者-消費者模型中的“實現二:wait && notify”,使用明確可讀的條件隊列。簡化如下:
public class WaitNotifyModel implements Model {private final Object BUFFER_LOCK = new Object(); ...private class ConsumerImpl extends AbstractConsumer implements Consumer, Runnable {public void consume() throws InterruptedException {synchronized (BUFFER_LOCK) {while (buffer.size() == 0) {BUFFER_LOCK.wait();}Task task = buffer.poll();assert task != null;System.out.println("consume: " + task.no);BUFFER_LOCK.notifyAll();}}}private class ProducerImpl extends AbstractProducer implements Producer, Runnable {public void produce() throws InterruptedException {synchronized (BUFFER_LOCK) {while (buffer.size() == cap) {BUFFER_LOCK.wait();}Task task = new Task(increTaskNo.getAndIncrement());buffer.offer(task);System.out.println("produce: " + task.no);BUFFER_LOCK.notifyAll();}}} ... } 復制代碼BUFFER_LOCK即是內置的條件隊列。所有生產者線程和消費者線程都共享BUFFER_LOCK,通過BUFFER_LOCK的wait+notify機制實現同步。
- notify()和notifyAll()接下來講。
- 之所以命名為BUFFER_LOCK,是因為同時還要在將BUFFER_LOCK作為內置鎖來使用。命名為BUFFER_LOCK或BUFFER_COND都是可接受的。
notify()與notifyAll()
可以認為notify與wait是對偶的。s.wait()將當前線程c放入Object實例s的等待集合中,s.notify()隨機將一個線程t從s的等待集合中取出來(也可能不是隨機的,這取決于操作系統的實現。但很明顯JVM的使用者不應該依賴其是否隨機)。如果s的等待集合中有多個線程,那么t可能是剛才放入的線程c,也可能是其他線程。
雖然我們通常說“wait+notify”機制,但是使用更多的是notifyAll()而不是notify()。因為notify()只能喚醒一個線程,并且通常是隨機的——而被喚醒線程所等待的條件不一定已經被滿足(因為多個條件可以使用同一個條件隊列),從而會再次進入等待狀態;真正滿足了條件的線程卻因為沒被選中而繼續等待。這類似于“信號丟失”,可以稱為信號劫持。
notifyAll()則一次喚醒全部等待在該條件隊列上的線程。雖然notifyAll()解決了“信號劫持”的問題,但一次性喚醒全部線程去競爭鎖,也大大加劇了無效競爭。
關于notify()與notifyAll()的自問自答
如何同時解決信號劫持與無效競爭?
不過,只要保證notify()每次都能叫醒正確的人,就能在解決信號劫持的前提下,避免無效競爭。方法很簡單,禁止不同類型的線程共用條件隊列。具體來說:
- 一個條件隊列只用來維護一個條件
- 每個線程被喚醒后執行的操作相同
使用join()方法的過程中,沒有任何線程調用notify(),如何喚醒線程t1?
為了方便理解,前面事件8(線程t1中,Thread實例f發現Thread實例s不再存活)采用了不正確的描述。在事件8之前,線程t1已經處于阻塞狀態,從而Thread實例f無法發現s是否不再存活。那么,使用join()方法的過程中,沒有任何線程調用notify(),如何喚醒線程t1?
在線程t1死亡的時候,JVM會幫忙調用s.notify()(或非正常死亡時拋出InterruptedException),以喚醒線程t1;t1中做判斷,發現s不再存活,便能夠正常只是后面的邏輯。
這是必要的。假設JVM不會幫忙(調用s.notify()或拋出InterruptedException),在最壞的情況下,如果線程t1被用戶從操作系統中強制殺死,那么在條件隊列s上等待的主線程t0將永遠阻塞,而不知道此時發生的異常情況。
同時,這種幫助在JVM規范下沒有副作用。因為JVM要求用戶從wait()方法返回后檢查條件是否得到滿足。如果用戶編寫了錯誤的同步邏輯,使得線程t2正常執行結束后,條件仍不能得到滿足,那么雖然JVM的“幫助”使得線程t1提前喚醒,但wait()返回后的檢查使線程t1再次進入阻塞狀態,符合用戶編寫的同步邏輯(盡管是錯誤的)。另一方面,如果沒有線程等待條件隊列,那么notify也不會做任何事。
wait+notify的配合過程
仍然用Thread實例的狀態代替線程的狀態。
1. 調用wait()前
調用wait()前,線程t1對應的Thread實例f、t2對應的s都處于RUNNABLE狀態:
2. 調用wait()后,調用notify()前
在線程t1中調用s.wait()后,其他線程調用s.notify()前,t1對應的f轉入WAITING狀態,進入對象s的等待隊列(即,條件隊列);s不受影響,仍處于RUNNABLE狀態:
3. 調用notify()后
假設在主線程t0中主動調用s.notify(),那么在此之后,線程t1對應的Thread實例f轉入RUNNABLE狀態;s仍然不受影響:
本文鏈接:條件隊列大法好:wait和notify的基本語義
作者:猴子007
出處:monkeysayhi.github.io
本文基于?知識共享署名-相同方式共享 4.0?國際許可協議發布,歡迎轉載,演繹或用于商業目的,但是必須保留本文的署名及鏈接。
總結
以上是生活随笔為你收集整理的条件队列大法好:wait和notify的基本语义的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bzoj 5090 组题
- 下一篇: maven+tomcat8.0+ecli