美文网首页
Java中wait、notify与notifyAll

Java中wait、notify与notifyAll

作者: Ghamster | 来源:发表于2019-03-18 18:21 被阅读0次

博客发表于: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. 被唤醒的线程需要竞争临界资源,相比于其他线程不具有更高或更低的优先级

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

《全剧终》

相关文章

网友评论

      本文标题:Java中wait、notify与notifyAll

      本文链接:https://www.haomeiwen.com/subject/rlbbmqtx.html