Java中wait、notify與notifyAll

博客發(fā)表于:Ghamster Blog
轉(zhuǎn)載請(qǐng)注明出處

概述

Java中可使用waitnotify(或notifyAll)方法同步對(duì)臨界資源的訪問
這些方法在Object類中定義,因此可以在任何對(duì)象上調(diào)用
在對(duì)象上調(diào)用wait方法使線程阻塞,在對(duì)象上調(diào)用notifynotifyAll會(huì)喚醒之前在該對(duì)象上調(diào)用wait阻塞的線程
調(diào)用這些方法之前需要線程已經(jīng)獲取對(duì)象的鎖(This method should only be called by a thread that is the owner of this object's monitor),否則會(huì)拋出java.lang.IllegalMonitorStateException。因此只能在同步方法或同步代碼塊中使用

wait

  • 阻塞當(dāng)前線程,釋放鎖
  • wait()wait(0),為無限期阻塞(兩者完全相同);wait(long timeoutMillis)wait(long timeoutMillis, int nanos),若在參數(shù)指定時(shí)間內(nèi)沒有被喚醒或打斷,自動(dòng)恢復(fù)執(zhí)行
  • 可以被notifynotifyAll喚醒
  • 可以被interrupt方法打斷,拋出InterruptedException

notify & notifyAll

  • notify: 喚醒一個(gè)在該對(duì)象上調(diào)用wait方法阻塞的線程
  • notifyAll: 喚醒所有在該對(duì)象上調(diào)用wait方法阻塞的線程

notify與notifyAll測試

notify相對(duì)于notifyAll方法是一種性能優(yōu)化,因?yàn)?code>notify只會(huì)喚醒一個(gè)線程,但notifyAll會(huì)喚醒所有等待的線程,使他們競爭cpu;但同時(shí),使用notify你必須確定被喚醒的是合適的線程

下面的測試代碼展示了“必須喚醒合適線程的問題”

  • Critical類只包含一個(gè)Color類的對(duì)象,通過對(duì)象初始化語句賦值為Color.B
  • ColorModifier類實(shí)現(xiàn)了Runnable接口,包含三個(gè)域: criticaltargetto,操作criticalcolor對(duì)象,當(dāng)與目標(biāo)顏色target相符時(shí),將顏色修改為to指定的值。
  • main方法中,依次創(chuàng)建三個(gè)ColorModifier類的實(shí)例,分別為R->GG->B,B->R,交給ExectorService執(zhí)行,30s后關(guān)閉ExectorService,三個(gè)線程收到InterruptedException退出

使用notifyAll的測試代碼如下:

package main.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestNotify {

    enum Color {R, G, B}

    private static class Critical {
        public Color color = Color.R;
    }

    private static class ColorModifier implements Runnable {
        private Critical critical;
        private Color target;
        private Color to;

        public ColorModifier(Critical critical, Color target, Color to) {
            this.critical = critical;
            this.target = target;
            this.to = to;
        }

        @Override
        public void run() {
            System.out.printf("-> Thread start: Modifier %s to %s\n", target, to);
            try {
                while (!Thread.interrupted()) {
                    synchronized (critical) {
                        while (critical.color != target) {
                            System.out.printf("  - Wait: Modifier %s -> %s, Current color: %s\n", target, to, critical.color);
                            critical.wait();
                            System.out.printf("  + Resume from wait: Modifier %s -> %s, Current color: %s\n", target, to, critical.color);
                        }
                        //change critical.color and notify others
                        critical.color = to;
                        System.out.printf("\n>>> Color changed: %s to %s!\n", target, to);
                        TimeUnit.SECONDS.sleep(1);
                        critical.notifyAll();
                    }
                }
            } catch (InterruptedException e) {
                System.out.printf("Thread Modifier %s -> %s exit!\n", target, to);
            }

        }

        public static void main(String[] args) throws InterruptedException {
            ExecutorService exec = Executors.newCachedThreadPool();
            Critical c = new Critical();
            exec.execute(new ColorModifier(c, Color.R, Color.G));
            exec.execute(new ColorModifier(c, Color.G, Color.B));
            exec.execute(new ColorModifier(c, Color.B, Color.R));
            TimeUnit.SECONDS.sleep(30);
            exec.shutdownNow();
        }
    }
}

輸出如下:

-> Thread start: Modifier R to G

>>> Color changed: R to G!
-> Thread start: Modifier B to R
-> Thread start: Modifier G to B
  - Wait: Modifier R -> G, Current color: G

>>> Color changed: G to B!
  - Wait: Modifier G -> B, Current color: B

>>> Color changed: B to R!
  - Wait: Modifier B -> R, Current color: R
  + Resume from wait: Modifier G -> B, Current color: R
  - Wait: Modifier G -> B, Current color: R
  + Resume from wait: Modifier R -> G, Current color: R

>>> Color changed: R to G!
  - Wait: Modifier R -> G, Current color: G
  + Resume from wait: Modifier B -> R, Current color: G
  - Wait: Modifier B -> R, Current color: G
  + Resume from wait: Modifier G -> B, Current color: G

>>> Color changed: G to B!
  - Wait: Modifier G -> B, Current color: B
  + Resume from wait: Modifier R -> G, Current color: B
  - Wait: Modifier R -> G, Current color: B
  + Resume from wait: Modifier B -> R, Current color: B

>>> Color changed: B to R!
... ...
Thread Modifier B -> R exit!
Thread Modifier R -> G exit!
Thread Modifier G -> B exit!

Process finished with exit code 0

任意時(shí)刻,系統(tǒng)中有三個(gè)ColorModifier的線程(更嚴(yán)謹(jǐn)?shù)谋硎鍪牵簍arget為ColorModifer對(duì)象的線程)RtoG、GtoB和BtoR,假設(shè)RtoG修改顏色后(console第17行),調(diào)用notifyAll方法,使GtoB、BtoR線程被喚醒,三個(gè)線程均可開始(繼續(xù))執(zhí)行。當(dāng)前顏色為Color.G,執(zhí)行至代碼32行,RtoG和BtoR調(diào)用wait阻塞,GtoB修改顏色并調(diào)用notifyAll方法,如此往復(fù)

測試notify方法時(shí),將第40行代碼修改為critical.notify();,輸出如下:

-> Thread start: Modifier B to R
-> Thread start: Modifier R to G
-> Thread start: Modifier G to B
  - Wait: Modifier B -> R, Current color: R
  - Wait: Modifier G -> B, Current color: R

>>> Color changed: R to G!
  - Wait: Modifier R -> G, Current color: G
  + Resume from wait: Modifier B -> R, Current color: G
  - Wait: Modifier B -> R, Current color: G
Thread Modifier B -> R exit!
Thread Modifier R -> G exit!
Thread Modifier G -> B exit!

Process finished with exit code 0

每次運(yùn)行測試得到的輸出各不相同,但幾乎所有的測試都會(huì)導(dǎo)致死鎖,直到時(shí)間耗盡,調(diào)用ExectorService.shutdownNow()結(jié)束程序。以本次運(yùn)行結(jié)果為例,RtoG、GtoB和BtoR依次啟動(dòng),Critical對(duì)象初始顏色為Color.R。執(zhí)行至代碼32行,BtoR和GtoB調(diào)用wait阻塞(對(duì)應(yīng)console第4-5行);RtoG將顏色修改為Color.G,調(diào)用notify方法,BtoR被喚醒;RtoG繼續(xù)執(zhí)行,經(jīng)過代碼32行判斷后調(diào)用wait阻塞;BtoR被喚醒后,經(jīng)過32行同樣調(diào)用wait阻塞 -- 至此三個(gè)線程全部阻塞,程序陷入死鎖。

對(duì)于本程序而言,“合適的線程”是指:BtoR的notify必須喚醒RtoG,RtoG的notify必須喚醒GtoB,GtoB的notify必須喚醒BtoR

one more thing

如果對(duì)測試代碼稍作修改會(huì)發(fā)生有趣的事情:

  1. Critical對(duì)象的color屬性初始值設(shè)為Color.B(12行)
  2. main方法的每個(gè)exec.execute()方法后插入TimeUnit.SECONDS.sleep(1);,插入后代碼如下:
public static void main(String[] args) throws InterruptedException {
    ExecutorService exec = Executors.newCachedThreadPool();
    Critical c = new Critical();
    exec.execute(new ColorModifier(c, Color.R, Color.G));
    TimeUnit.SECONDS.sleep(1);
    exec.execute(new ColorModifier(c, Color.G, Color.B));
    TimeUnit.SECONDS.sleep(1);
    exec.execute(new ColorModifier(c, Color.B, Color.R));
    TimeUnit.SECONDS.sleep(30);
    exec.shutdownNow();
}

此時(shí)會(huì)得到如下輸出:

>>> Color changed: B to R!
  - Wait: Modifier B -> R, Current color: R
  + Resume from wait: Modifier R -> G, Current color: R

>>> Color changed: R to G!
  - Wait: Modifier R -> G, Current color: G
  + Resume from wait: Modifier G -> B, Current color: G

>>> Color changed: G to B!
  - Wait: Modifier G -> B, Current color: B
  + Resume from wait: Modifier B -> R, Current color: B

程序并未出現(xiàn)死鎖!似乎BtoR的notify總會(huì)喚醒RtoG,RtoG會(huì)喚醒GtoB,GtoB會(huì)喚醒BtoR
換言之,notify被調(diào)用時(shí),喚醒的線程不是隨機(jī)的,而是所有阻塞的線程中,最早調(diào)用wait的那個(gè)

測試

測試環(huán)境:window x64,jdk11

  • 內(nèi)部類WaitAndNotify實(shí)現(xiàn)了Runnable接口,構(gòu)造方法需要傳入一個(gè)Object對(duì)象(o)
  • run方法中,首先調(diào)用o.wait()阻塞,被喚醒后調(diào)用o.notify()
  • main方法依次產(chǎn)生THREAD_NUMBERS個(gè)使用WaitAndNotify對(duì)象創(chuàng)建的線程,并交由ExecutorService執(zhí)行;在main方法中調(diào)用notify,引發(fā)鏈?zhǔn)椒磻?yīng),使所有線程依次執(zhí)行
  • 使用CountDownLatch計(jì)數(shù),所有線程完成后,關(guān)閉ExecutorService退出程序

測試代碼如下:

package main.test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestSynchronizedLockOrder {

    private static final int THREAD_NUMBERS = 5;

    private static class WaitAndNotify implements Runnable {
        private static int count = 0;
        private int id = count++;
        private CountDownLatch countDownLatch;
        private Object o;

        public WaitAndNotify(Object o, CountDownLatch c) {
            this.o = o;
            this.countDownLatch = c;
        }

        @Override
        public void run() {
            synchronized (o) {
                try {
                    System.out.println("WAN id=" + id + " call wait");
                    o.wait();
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("WAN id=" + id + " running");
                    o.notify();
                    System.out.println("WAN id=" + id + " call notify");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUMBERS);
        ExecutorService e = Executors.newCachedThreadPool();
        for (int i = 0; i < THREAD_NUMBERS; i++) {
            e.execute(new WaitAndNotify(o, countDownLatch));
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println("===================\nAll thread started!\n===================");
        synchronized (o) {
            o.notify();
        }
        countDownLatch.await();
        e.shutdownNow();
    }
}

程序輸出如下:

WAN id=0 call wait
WAN id=1 call wait
WAN id=2 call wait
WAN id=3 call wait
WAN id=4 call wait
===================
All thread started!
===================
WAN id=0 running
WAN id=0 call notify
WAN id=1 running
WAN id=1 call notify
WAN id=2 running
WAN id=2 call notify
WAN id=3 running
WAN id=3 call notify
WAN id=4 running
WAN id=4 call notify

Process finished with exit code 0

結(jié)論

顯然,在本平臺(tái)上調(diào)用notify方法時(shí),被喚醒的永遠(yuǎn)是最早調(diào)用wait方法阻塞的線程,但這個(gè)結(jié)論是否具有普遍性?

jdk文檔對(duì)于notify的描述如下:

Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation...
The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object...

參考jdk文檔的內(nèi)容,總結(jié)來說有兩點(diǎn):

  1. 調(diào)用notify方法會(huì)喚醒一個(gè)阻塞的線程,且這個(gè)線程是隨機(jī)的,且不同平臺(tái)可以有不同實(shí)現(xiàn)
  2. 被喚醒的線程需要競爭臨界資源,相比于其他線程不具有更高或更低的優(yōu)先級(jí)

因此,這種測試結(jié)果只能算平臺(tái)的特例……

《全劇終》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.解決信號(hào)量丟失和假喚醒 public class MyWaitNotify3{ MonitorObject m...
    Q羅閱讀 1,017評(píng)論 0 1
  • 一、進(jìn)程和線程 進(jìn)程 進(jìn)程就是一個(gè)執(zhí)行中的程序?qū)嵗?,每個(gè)進(jìn)程都有自己獨(dú)立的一塊內(nèi)存空間,一個(gè)進(jìn)程中可以有多個(gè)線程。...
    阿敏其人閱讀 2,714評(píng)論 0 13
  • 相關(guān)概念 面向?qū)ο蟮娜齻€(gè)特征 封裝,繼承,多態(tài).這個(gè)應(yīng)該是人人皆知.有時(shí)候也會(huì)加上抽象. 多態(tài)的好處 允許不同類對(duì)...
    東經(jīng)315度閱讀 2,212評(píng)論 0 8
  • 本文出自 Eddy Wiki ,轉(zhuǎn)載請(qǐng)注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 2,304評(píng)論 0 14
  • 1、概述 Java 給多線程編程提供了內(nèi)置的支持。 一條線程指的是進(jìn)程中一個(gè)單一順序的控制流,一個(gè)進(jìn)程中可以并發(fā)多...
    高丕基閱讀 564評(píng)論 0 8

友情鏈接更多精彩內(nèi)容