Java中wait、notify与notifyAll

博客发表于:Ghamster Blog
转载请注明出处

概述

Java中可使用waitnotify(或notifyAll)方法同步对临界资源的访问
这些方法在Object类中定义,因此可以在任何对象上调用
在对象上调用wait方法使线程阻塞,在对象上调用notifynotifyAll会唤醒之前在该对象上调用wait阻塞的线程
调用这些方法之前需要线程已经获取对象的锁(This method should only be called by a thread that is the owner of this object's monitor),否则会抛出java.lang.IllegalMonitorStateException。因此只能在同步方法或同步代码块中使用

wait

  • 阻塞当前线程,释放锁
  • wait()wait(0),为无限期阻塞(两者完全相同);wait(long timeoutMillis)wait(long timeoutMillis, int nanos),若在参数指定时间内没有被唤醒或打断,自动恢复执行
  • 可以被notifynotifyAll唤醒
  • 可以被interrupt方法打断,抛出InterruptedException

notify & notifyAll

  • notify: 唤醒一个在该对象上调用wait方法阻塞的线程
  • notifyAll: 唤醒所有在该对象上调用wait方法阻塞的线程

notify与notifyAll测试

notify相对于notifyAll方法是一种性能优化,因为notify只会唤醒一个线程,但notifyAll会唤醒所有等待的线程,使他们竞争cpu;但同时,使用notify你必须确定被唤醒的是合适的线程

下面的测试代码展示了“必须唤醒合适线程的问题”

  • Critical类只包含一个Color类的对象,通过对象初始化语句赋值为Color.B
  • ColorModifier类实现了Runnable接口,包含三个域: criticaltargetto,操作criticalcolor对象,当与目标颜色target相符时,将颜色修改为to指定的值。
  • main方法中,依次创建三个ColorModifier类的实例,分别为R->GG->BB->R,交给ExectorService执行,30s后关闭ExectorService,三个线程收到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

任意时刻,系统中有三个ColorModifier的线程(更严谨的表述是:target为ColorModifer对象的线程)RtoG、GtoB和BtoR,假设RtoG修改颜色后(console第17行),调用notifyAll方法,使GtoB、BtoR线程被唤醒,三个线程均可开始(继续)执行。当前颜色为Color.G,执行至代码32行,RtoG和BtoR调用wait阻塞,GtoB修改颜色并调用notifyAll方法,如此往复

测试notify方法时,将第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

每次运行测试得到的输出各不相同,但几乎所有的测试都会导致死锁,直到时间耗尽,调用ExectorService.shutdownNow()结束程序。以本次运行结果为例,RtoG、GtoB和BtoR依次启动,Critical对象初始颜色为Color.R。执行至代码32行,BtoR和GtoB调用wait阻塞(对应console第4-5行);RtoG将颜色修改为Color.G,调用notify方法,BtoR被唤醒;RtoG继续执行,经过代码32行判断后调用wait阻塞;BtoR被唤醒后,经过32行同样调用wait阻塞 -- 至此三个线程全部阻塞,程序陷入死锁。

对于本程序而言,“合适的线程”是指:BtoR的notify必须唤醒RtoG,RtoG的notify必须唤醒GtoB,GtoB的notify必须唤醒BtoR

one more thing

如果对测试代码稍作修改会发生有趣的事情:

  1. Critical对象的color属性初始值设为Color.B(12行)
  2. main方法的每个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();
}

此时会得到如下输出:

>>> 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

程序并未出现死锁!似乎BtoR的notify总会唤醒RtoG,RtoG会唤醒GtoB,GtoB会唤醒BtoR
换言之,notify被调用时,唤醒的线程不是随机的,而是所有阻塞的线程中,最早调用wait的那个

测试

测试环境:window x64,jdk11

  • 内部类WaitAndNotify实现了Runnable接口,构造方法需要传入一个Object对象(o)
  • run方法中,首先调用o.wait()阻塞,被唤醒后调用o.notify()
  • main方法依次产生THREAD_NUMBERS个使用WaitAndNotify对象创建的线程,并交由ExecutorService执行;在main方法中调用notify,引发链式反应,使所有线程依次执行
  • 使用CountDownLatch计数,所有线程完成后,关闭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

结论

显然,在本平台上调用notify方法时,被唤醒的永远是最早调用wait方法阻塞的线程,但这个结论是否具有普遍性?

jdk文档对于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文档的内容,总结来说有两点:

  1. 调用notify方法会唤醒一个阻塞的线程,且这个线程是随机的,且不同平台可以有不同实现
  2. 被唤醒的线程需要竞争临界资源,相比于其他线程不具有更高或更低的优先级

因此,这种测试结果只能算平台的特例……

《全剧终》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 871评论 0 1
  • 一、进程和线程 进程 进程就是一个执行中的程序实例,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程。...
    阿敏其人阅读 2,607评论 0 13
  • 相关概念 面向对象的三个特征 封装,继承,多态.这个应该是人人皆知.有时候也会加上抽象. 多态的好处 允许不同类对...
    东经315度阅读 1,925评论 0 8
  • 本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/interview-java.h...
    eddy_wiki阅读 2,054评论 0 14
  • 1、概述 Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多...
    高丕基阅读 475评论 0 8