wait方法是当前拥有了锁的线程发现自己不满足自己想要的运行条件,于是释放锁,进入wait状态,等待满足条件以后被唤醒
1. 为什么要使用wait和notify
假设现在有一些员工(线程
)要使用同一把算盘(共享资源
)工作,为了保证线程的安全性,他们的老板就设置了一个房间(synchronized
锁),在一个时间只有一个线程可以进入这个房间进行工作,但是现在,有一个员工小南进入了房间,但是他有一个毛病,必须得要有烟才能工作(满足条件
),所以没有烟的时候他就一直占用着房间还不工作
于是老板单开了一间休息室(调用 wait
方法),让小南到休息室(WaitSet
)等着去了,但这时锁释放开, 其它人可以由老板随机安排进屋
直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify
方法)
小南于是可以离开休息室,重新进入竞争锁的队列
2. 正确使用姿势
obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
obj.wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
//和sleep区分一下
//sleep 是 Thread 方法,而 wait 是 Object 的方法
//sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
//sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
//它们 状态 TIMED_WAITING
① notify/wait
都要在synchornized
代码块内,否则有java.lang.IllegalMonitorStateException
错
准确的说,必须获得了锁以后才能使用,这点很好理解,针对上面的例子就知道,你是在获得了锁但是没有满足自己想运行的条件,才会想去释放锁进行等待,
② 当获得锁的线程不满足条件就调用wait
释放锁,进入等待
③ notify
只能随机唤醒一个 WaitSet
中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒
】 解决方法,改为 notifyAll
③ 外部使用notifyAll()
唤醒所有,但是现在唤醒的不一定满足了他想要的条件,所有还有使用 while + wait
,当条件不成立,再次 wait
synchronized (lock){
while(条件不成立){//条件不成立继续等待
lock.wait();
}
}
synchronized (lock){
lock.notifyAll();//唤醒所有
}
所以针对上面的场景的实现应该是这样的
public class CigTest {
static final Object room = new Object();//定义成final是为了让他引用的对象不再改变,保证每次都加的同一把锁
static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
synchronized (room) {
System.out.println("小南准备工作");
System.out.println("小南问: 有烟么");
while (!hasCigarette) {
System.out.println("小南: 没有烟 我要去休息室");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//小南: 有烟了,我要回等待队列了
System.out.println("开始工作");
}
}
};
Thread nanMan = new Thread(runnable, "小南");
nanMan.start();
Thread.sleep(1);
new Thread(()->{
synchronized (room){
hasCigarette = true;
room.notifyAll();
}
}).start();
Thread.sleep(2);
}
}
3 使用wait/notify实现生产者消费者队列
/**
* 描述: 用wait/notify来实现生产者消费者模式
*/
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable {
private EventStorage storage;
public Producer(
EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(
EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里有了" + storage.size() + "个产品。");
notify();
}
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
notify();
}
}
4. 用程序交替打印0-100的奇偶数
4.1 使用synchronized实现
/**
* 描述: 两个线程交替打印0~100的奇偶数,用synchronized关键字实现
*/
public class WaitNotifyPrintOddEvenSyn {
private static int count;
private static final Object lock = new Object();
//新建2个线程
//1个只处理偶数,第二个只处理奇数(用位运算)
//用synchronized来通信
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 0) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}
}, "偶数").start();
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}
}, "奇数").start();
}
}
这种实现方式可能会造成A线程不断循环(不进入if语句块)直到B线程拿到锁,造成资源的浪费
4.2 使用wait/notify实现
/**
* 描述: 两个线程交替打印0~100的奇偶数,用wait和notify
*/
public class WaitNotifyPrintOddEveWait {
private static int count = 0;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new TurningRunner(), "偶数").start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new TurningRunner(), "奇数").start();
}
//1. 拿到锁,我们就打印
//2. 打印完,唤醒其他线程,自己就休眠
static class TurningRunner implements Runnable {
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
//拿到锁就打印
System.out.println(Thread.currentThread().getName() + ":" + count++);
lock.notify();
if (count <= 100) {
try {
//如果任务还没结束,就让出当前的锁,并休眠
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
5. 为什么wait()需要在同步代码块中使用而sleep不需要
① wait使用的前提时占有锁
② 如果把wait()或者notify()放到同步代码块外面是的没有同步代码块的保护可能会导致notify()先于wait执行而wait就再也没有被唤醒的机会了
6. 为什么线程通信的方法wait(),notify()被定义在Object类中,而sleep()定义在Object类中
因为他们是锁级别的操作,一个线程可能持有多把锁,如果定义在Thread类中就无法使用多把锁灵活配合了
7. 底层原理
如果使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象(Monitor对象是操作系统的对象)的指针,详见管程
-
Owner
线程发现条件不满足,调用wait
方法,即可进入WaitSet
变为WAITING
状态 -
BLOCKED
和WAITING
的线程都处于阻塞状态,不占用 CPU 时间片 -
BLOCKED
线程会在Owner
线程释放锁时唤醒 -
WAITING
线程会在Owner
线程调用notify
或notifyAll
时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList
重新竞争