详细解析wait/notify

目录

1、为什么需要wait

2、底层原理

3、API 用法

4、尽量使用notifyAll()

5、sleep()和wait()的区别

6、正确使用wait/notify

7、wait/notify和join

8、为什么wait是Object的方法


1、为什么需要wait

在一个线程运行的过程中,如果发现条件不满足,就应该去到一个WaitSet,让出CPU运行资源(释放锁)

等待条件满足了,再由其他线程通知它(调用notify),该线程从WaitSet中转移到阻塞队列,等待被运行。

特别注意:

  • wait方法是Object的,它并不是Thread的方法,也就是说,只能在锁对象上调用wait,线程对象是不能调用wait的!
  • wait的逻辑是:
    • 一个线程想要让自己等待,就在线程内部调用锁的wait方法
    • 一个线程想要唤醒等待的线程,就调用锁的notify或notifyAll方法

2、底层原理

  • Owner线程发现条件不满足,就调用wait方法,进入了WaitSet,变为Waiting状态
  • Blocked和Waiting的线程都处于阻塞状态,不同之处在于:
    • Blocked线程会在Owner线程释放锁时唤醒
    • Waiting线程会在Owner线程调用notify或notifyAll时唤醒,但不会立即运行,而是进入了阻塞队列等待被调度。

3、API 用法

必须获得obj对象的锁(即在synchrozed内),才能调用这些方法。

  • obj.wait():让obj的monitor中Owner的线程,进入WaitSet中等待
  • obj.notify():在obj的monitor中WaitSet中等待的线程,挑一个唤醒
  • obj.notifyAll():在obj的monitor中WaitSet中等待的线程,全部唤醒

注意:

  • 一个线程调用 锁对象.wait() 后,会立即释放锁,然后阻塞
  • 一个线程调用 锁对象.notify或notifyAll后,不会立即释放锁,等到线程执行完才会释放锁。
    • 但是,调用完锁对象.notify或notifyAll后,Waiting的线程就苏醒了,之后进入锁的阻塞队列
    • 等到该线程释放锁后,它们竞争锁,竞争到的运行,没竞争到的继续阻塞

还有一个带参数的方法:obj.wait(long n),等待n毫秒后自动唤醒,期间可以被notify

Object中wait()的本质,就是调用了wait(0),无限期等待。

4、尽量使用notifyAll()

一般用notifyAll(),因为notify()每次只会唤醒一个线程,有可能导致某些线程永远不会被通知到。

除非深思熟虑,不然不要使用notify()。可以用notify()的地方满足三个条件:

  • 所有等待线程拥有相同的等待条件
  • 所有等待线程被唤醒后,执行相同的操作
  • 只需要唤醒一个线程

5、sleep()和wait()的区别

不同点:

  • sleep是Thread的静态方法,而wait是Object的实例方法
  • sleep调用时不需要获取锁,wait调用时必须获取锁(和synchrozed一起使用)
  • sleep不会释放锁,而wait会释放锁
  • sleep(0)的含义是睡眠0毫秒,让CPU重新调度线程,wait(0)的含义是无限期等待

共同点:

  • 调用Thread.sleep(long n)和Object.wait(long n)后,线程的状态都是Timed_Waiting,被interrupt()打断后都会抛出异常。

6、正确使用wait/notify

涉及到两个设计模式:

  • 保护性暂停
  • 生产者消费者

前提描述:多个线程使用同一把锁,其中的小南线程必须有烟才能工作,其他线程没有条件,运行后就能工作

//锁对象
static final Object room = new Object();
//小南线程所需的条件
static boolean hasCigarette = false;
//小女线程所需的条件
static boolean hasTakeout = false

情形一

new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            sleep(2);
        }
        log.debug("有烟没?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以开始干活了");
        }
    }
}, "小南").start();

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        synchronized (room) {
            log.debug("可以开始干活了");
        }
    }, "其它人").start();
}

sleep(1);
new Thread(() -> {
    // 这里能不能加 synchronized (room)?
    hasCigarette = true;
    log.debug("烟到了噢!");
}, "送烟的").start();

这种处理很不好:

  • 小南线程运行需要条件,而sleep不会释放锁,影响了其他线程的运行,应该改成使用wait
  • 在sleep的期间,就算条件满足了,也无法及时醒来。不过可以使用interrupt来唤醒线程
  • 如果在下面的送烟线程加上synchronized (room),由于小南线程一直持有锁,所以烟一直送不到。

总结:在有线程需要等待条件满足时,使用 wait - notify 机制。

情形二

new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            try {
                room.wait(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("有烟没?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以开始干活了");
        }
    }
}, "小南").start();

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        synchronized (room) {
            log.debug("可以开始干活了");
        }
    }, "其他人").start();
}

sleep(1);
new Thread(() -> {
    synchronized (room) {
        hasCigarette = true;
        log.debug("烟到了噢!");
        room.notify();
    }
}, "送烟的").start();

这样就比较合适,但如果存在多个线程都在等待条件?

情形三

new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            try {
                room.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("有烟没?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以开始干活了");
        } else {
            log.debug("没干成活...");
        }
    }
}, "小南").start();

new Thread(() -> {
    synchronized (room) {
        Thread thread = Thread.currentThread();
        log.debug("外卖送到没?[{}]", hasTakeout);
        if (!hasTakeout) {
            log.debug("没外卖,先歇会!");
            try {
                room.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("外卖送到没?[{}]", hasTakeout);
        if (hasTakeout) {
            log.debug("可以开始干活了");
        } else {
            log.debug("没干成活...");
        }
    }
}, "小女").start();

sleep(1);
new Thread(() -> {
    synchronized (room) {
        hasTakeout = true;
        log.debug("外卖到了噢!");
        room.notify();
    }
}, "送外卖的").start();

这就存在一个问题。专门用一个线程来唤醒,但调用的是notify(),随机唤醒一个Waiting的线程。

此时小南、小女都在等待,但满足的是小女的条件。如果被唤醒的是小南,那么小南运行后发现没有满足条件,而小女就没有被唤醒的机会了。

这种情况称之为“虚假唤醒”,即notify()唤醒的并不是我们希望被唤醒的那个线程,这会在存在多个线程等待时发生。

解决方案:存在多个线程等待时,使用notifyAll(),把所有等待的线程都唤醒,让他们自行判断条件是否被满足了。

情形四

new Thread(() -> {
    synchronized (room) {
        hasTakeout = true;
        log.debug("外卖到了噢!");
        room.notifyAll();
    }
}, "送外卖的").start();

这种notifyAll()的做法很合适,但是小南小女线程的做法有问题。

把小南线程拿下来:

new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            try {
                room.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("有烟没?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以开始干活了");
        } else {
            log.debug("没干成活...");
        }
    }
}, "小南").start();

如果对条件的判断写成if,那么第一次运行时发现条件不满足,就会wait。但第二次被唤醒后,如果发现还是没满足,线程就会直接结束。

解决方案:把对条件的判断改成while + wait,这样每次被唤醒后就可以检查条件是否满足,不满足就继续wait。

情形五

while (!hasCigarette) {
    log.debug("没烟,先歇会!");
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

把对条件的判断改成了while + wait,这样就没有问题了。

总结

合理的写法:

synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    // 干活
}

//另一个线程
synchronized(lock) {
    lock.notifyAll();
}

7、wait/notify和join

join有两个缺点:

  • join必须等待该线程执行结束,当前线程才能继续运行。而合理编写wait/notify,可以使得资源一旦计算出来就立刻唤醒线程,效率更高
  • 使用join的话,等待的结果只能设置成全局的,而不能设置成局部的。

8、为什么wait是Object的方法

wait会释放锁,然后进入monitor的等待队列。

既然所有的对象都可以作为锁,那么Object类是所有类的父类,所以wait应该定义在Object类中,操作所有对象的monitor。

同理,notify和notifyAll方法也都定义在了Object类中。

至于具体的实现,涉及到对底层的操作,所以wait是一个native的方法,底层使用c来实现。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值