Java中wait、notify与notifyAll

2019-03-18  本文已影响0人  Ghamster

博客发表于: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

notify & notifyAll

notify与notifyAll测试

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

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

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

测试代码如下:

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

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

《全剧终》

上一篇 下一篇

猜你喜欢

热点阅读