sleep/wait/notify/notifyAll 分别有什么作用? 它们的区别是什么? wait 时为什么要放在循环里而不能直接用 if?
简介
首先对几个相关的方法做个简单解释, Object 中有几个用于线程同步的方法: wait,notify,notifyAll.
- public class Object {
- public final native void wait(long timeout) throws InterruptedException;
- public final native void notify();
- public final native void notifyAll();
- }
wait: 释放当前锁, 阻塞直到被 notify 或 notifyAll 唤醒, 或者超时, 或者线程被中断(InterruptedException)
notify: 任意选择一个 (无法控制选哪个) 正在这个对象上等待的线程把它唤醒, 其它线程依然在等待被唤醒
notifyAll: 唤醒所有线程, 让它们去竞争, 不过也只有一个能抢到锁
sleep: 不是 Object 中的方法, 而是 Thread 类的静态方法, 让当前线程持有锁阻塞指定时间
sleep 和 wait
sleep 和 wait 都可以让线程阻塞, 也都可以指定超时时间, 甚至还都会抛出中断异常 InterruptedException.
而它们最大的区别就在于, sleep 时线程依然持有锁, 别人无法进当前同步方法; wait 时放弃了持有的锁, 其它线程有机会进入该同步方法. 多次提到同步方法, 因为 wait 必须在 synchronized 同步代码块中, 否则会抛出异常 IllegalMonitorStateException,notify 也是如此, 可以说 wait 和 notify 是就是为了在同步代码中做线程调度而生的.
下面一个简单的例子展现 sleep 和 wait 的区别:
- import java.util.Date;
- import java.util.concurrent.atomic.AtomicInteger;
- public class Main {
- // 日志行号记录
- private AtomicInteger count = new AtomicInteger();
- public static void main(String[] args) throws InterruptedException {
- Main main = new Main();
- // 开启两个线程去执行 test 方法
- new Thread(main::test).start();
- new Thread(main::test).start();
- }
- private synchronized void test() {
- try {
- log("进入了同步方法, 并开始睡觉, 1s");
- // sleep 不会释放锁, 因此其他线程不能进入这个方法
- Thread.sleep(1000);
- log("睡好了, 但没事做, 有事叫我, 等待 2s");
- // 阻塞在此, 并且释放锁, 其它线程可以进入这个方法
- // 当其它线程调用此对象的 notify 或者 notifyAll 时才有机会停止阻塞
- // 就算没有人 notify, 如果超时了也会停止阻塞
- wait(2000);
- log("我要走了, 但我要再睡一觉, 10s");
- // 这里睡的时间很长, 因为没有释放锁, 其它线程就算 wait 超时了也无法继续执行
- Thread.sleep(10000);
- log("走了");
- notify();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- // 打印日志
- private void log(String s) {
- System.out.println(count.incrementAndGet() + " "
- + new Date().toString().split(" ")[3]
- + "\t" + Thread.currentThread().getName() + " " + s);
- }
- }
- /* 输出:
- 1 00:13:23 Thread-0 进入了同步方法, 并开始睡觉, 1s
- 2 00:13:24 Thread-0 睡好了, 但没事做, 有事叫我, 等待 2s
- 3 00:13:24 Thread-1 进入了同步方法, 并开始睡觉, 1s
- 4 00:13:25 Thread-1 睡好了, 但没事做, 有事叫我, 等待 2s
- 5 00:13:26 Thread-0 我要走了, 但我要再睡一觉, 10s
- 6 00:13:36 Thread-0 走了
- 7 00:13:36 Thread-1 我要走了, 但我要再睡一觉, 10s
- 8 00:13:46 Thread-1 走了
- */
对输出做个简单解释(已经看懂代码的童鞋可以跳过):
00:13:23 Thread-0 进入了同步方法, 并开始睡觉, 1s // Thread-0 首先进入同步方法, Thread-1 只能门外候着
00:13:24 Thread-0 睡好了, 但没事做, 有事叫我, 等待 2s // Thread-0 sleep 1 秒这段时间, Thread-1 没进来, 证明 sleep 没有释放锁
00:13:24 Thread-1 进入了同步方法, 并开始睡觉, 1s // Thread-0 开始 wait 后 Thread-1 马上就进来了, 证明 wait 释放了锁
00:13:25 Thread-1 睡好了, 但没事做, 有事叫我, 等待 2s // Thread-1 也打算 wait 2 秒(2 秒后真的能醒来吗?)
00:13:26 Thread-0 我要走了, 但我要再睡一觉, 10s // Thread-0 已经 wait 超时醒来了, 这次准备 sleep 10s
00:13:36 Thread-0 走了 // 10s 过去了 Thread-0 都 sleep 结束了, 那个说要 wait 2s 的 Thread-1 还没动静, 证明超时也没用, 还得抢到锁
00:13:36 Thread-1 我要走了, 但我要再睡一觉, 10s // Thread-0 退出同步代码后, Thread-1 才终于得到了锁, 能行动了
00:13:46 Thread-1 走了
notify 和 notifyAll
同样是唤醒等待的线程, 同样最多只有一个线程能获得锁, 同样不能控制哪个线程获得锁.
区别在于:
notify: 唤醒一个线程, 其他线程依然处于 wait 的等待唤醒状态, 如果被唤醒的线程结束时没调用 notify, 其他线程就永远没人去唤醒, 只能等待超时, 或者被中断
notifyAll: 所有线程退出 wait 的状态, 开始竞争锁, 但只有一个线程能抢到, 这个线程执行完后, 其他线程又会有一个幸运儿脱颖而出得到锁
如果觉得解释的不够明白, 代码来一波:
- import java.util.Date;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.atomic.AtomicInteger;
- public class Main {
- private AtomicInteger count = new AtomicInteger();
- public static void main(String[] args) throws InterruptedException {
- Main main = new Main();
- // 开启两个线程去执行 test 方法
- for (int i = 0; i < 10; i++) {
- new Thread(main::testWait).start();
- }
- Thread.sleep(1000);
- for (int i = 0; i < 5; i++) {
- main.testNotify();
- }
- }
- private synchronized void testWait() {
- try {
- log("进入了同步方法, 开始 wait");
- wait();
- log("wait 结束");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- private synchronized void testNotify() {
- notify();
- }
- private void log(String s) {
- System.out.println(count.incrementAndGet() + " "
- + new Date().toString().split(" ")[3]
- + "\t" + Thread.currentThread().getName() + " " + s);
- }
- }
- /* 输出:
- 1 00:59:32 Thread-0 进入了同步方法, 开始 wait
- 2 00:59:32 Thread-9 进入了同步方法, 开始 wait
- 3 00:59:32 Thread-8 进入了同步方法, 开始 wait
- 4 00:59:32 Thread-7 进入了同步方法, 开始 wait
- 5 00:59:32 Thread-6 进入了同步方法, 开始 wait
- 6 00:59:32 Thread-5 进入了同步方法, 开始 wait
- 7 00:59:32 Thread-4 进入了同步方法, 开始 wait
- 8 00:59:32 Thread-3 进入了同步方法, 开始 wait
- 9 00:59:32 Thread-2 进入了同步方法, 开始 wait
- 10 00:59:32 Thread-1 进入了同步方法, 开始 wait
- 11 00:59:33 Thread-0 wait 结束
- 12 00:59:33 Thread-6 wait 结束
- 13 00:59:33 Thread-7 wait 结束
- 14 00:59:33 Thread-8 wait 结束
- 15 00:59:33 Thread-9 wait 结束
- */
例子中有 10 个线程在 wait, 但 notify 了 5 次, 然后其它线程一直阻塞, 这也就说明使用 notify 时如果不能准确控制和 wait 的线程数对应, 可能会导致某些线程永远阻塞.
使用 notifyAll 唤醒所有等待的线程:
- import java.util.Date;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.atomic.AtomicInteger;
- public class Main {
- private AtomicInteger count = new AtomicInteger();
- public static void main(String[] args) throws InterruptedException {
- Main main = new Main();
- // 开启两个线程去执行 test 方法
- for (int i = 0; i < 5; i++) {
- new Thread(main::testWait).start();
- }
- Thread.sleep(1000);
- main.testNotifyAll();
- }
- private synchronized void testWait() {
- try {
- log("进入了同步方法, 开始 wait");
- wait();
- log("wait 结束");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- private synchronized void testNotifyAll() {
- notifyAll();
- }
- private void log(String s) {
- System.out.println(count.incrementAndGet() + " "
- + new Date().toString().split(" ")[3]
- + "\t" + Thread.currentThread().getName() + " " + s);
- }
- }
- /* 输出:
- 1 01:03:24 Thread-0 进入了同步方法, 开始 wait
- 2 01:03:24 Thread-4 进入了同步方法, 开始 wait
- 3 01:03:24 Thread-3 进入了同步方法, 开始 wait
- 4 01:03:24 Thread-2 进入了同步方法, 开始 wait
- 5 01:03:24 Thread-1 进入了同步方法, 开始 wait
- 6 01:03:25 Thread-1 wait 结束
- 7 01:03:25 Thread-2 wait 结束
- 8 01:03:25 Thread-3 wait 结束
- 9 01:03:25 Thread-4 wait 结束
- 10 01:03:25 Thread-0 wait 结束
- */
只需要调用一次 notifyAll, 所有的等待线程都被唤醒, 并且去竞争锁, 然后依次 (无序) 获取锁完成了后续任务.
为什么 wait 要放到循环中使用
一些源码中出现 wait 时, 往往都是伴随着一个循环语句出现的, 比如:
- private synchronized void f() throws InterruptedException {
- while (!isOk()) {
- wait();
- }
- System.out.println("I'm ok");
- }
既然 wait 会被阻塞直到被唤醒, 那么用 if+wait 不就可以了吗? 其他线程发现条件达到时 notify 一下不就行了?
理想情况确实如此, 但实际开发中我们往往不能保证这个线程被 notify 时条件已经满足了, 因为很可能有某个无关 (和这个条件的逻辑无关) 的线程因为需要线程调度而调用了 notify 或者 notifyAll. 此时如果样例中位置等待的线程不巧被唤醒, 它就会继续往下执行, 但因为用的 if, 这次被唤醒就不会再判断条件是否满足, 最终程序按照我们不期望的方式执行下去.
来源: https://www.cnblogs.com/acupt/p/11471255.html