1. 引入wait/notify

回顾:在之前的学习中,当我们创建一个对象后,synchronized给对象上锁,JVM会给对象头关联一个Monitor对象,这个Monitor由三部分组成。
一是Owner对象,里面存储的是创建该对象的线程
二是EntryList,想试图获取该对象资源的其它堵塞线程队列
三是WaitSet,存储的是放弃对象锁的线程

  • Owner线程中的锁对象,如果发现条件不满足,调用wait()方法,既可以进入到WaitSet变为WAITING状态
  • EntryList下和WaitSet下的线程都属于堵塞状态,不占用CPU时间片
  • EntryList下的线程会在Owner是释放锁时被唤醒
  • WaitSet下的线程会在Owner线程调用notify或notifyAll时被唤醒,但是唤醒后并不意味立刻获得锁,需要进入EntryList重新竞争锁

2. API介绍

obj.wait() 会让obj对象由拥有锁到暂时放弃锁,进入到waitset中
obj.notify() 会唤醒在waitset中的线程,然后需要进入EntryList重新竞争锁
obj.notifyAll() 会唤醒所有在waitset中的线程,然后需要进入EntryList重新竞争锁

case:

public class TestWaitNotify {
   
    final static Object obj = new Object();

    public static void main(String[] args) {
   

        new Thread(() -> {
   
            synchronized (obj) {
   
                log.debug("执行....");
                try {
   
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
   
            synchronized (obj) {
   
                log.debug("执行....");
                try {
   
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
   
// obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }

wait(long n)和wait()的区别是,前者是有时限的等待,时间到后自己进入EntryList中,后者如果没有notify()或notifyAll()唤醒则进入永久等待。

3.wait notify 的正确姿势

开始之前先看看sleep()和wait()方法有什么区别?

  • sleep()是Thread的静态方法,而wait()方法是Object类下的方法
  • sleep()方法不需要与synchronized配合使用,但是wait()方法必须和sysnchronized配合才能使用
  • 调用sleep()方法,对象不会释放锁,调用wait()方法时对象会释放锁
  • 相同点就是调用两个方法后,线程的状态都是TIMED_WAITING状态

case one:观察下面例子,有什么不妥的地方吗?

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
   
    final static Object room = new Object();
    static boolean hasCigarette = false;


    public static void main(String[] args) {
   

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

        //其它5个人
        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();
    }
}

输出:

不妥之处:

1. 在小南线程睡咩后,由于sleep()方法没有释放锁,导致其余5个人和送烟人都无法执行代码
2. 送烟人已经送到了,小南还在睡
3. 其它干活的线程,都要一直阻塞,效率太低

解决办法:使用wait()/notify()方法

case two:改进case one

public class TestWaitNotify {
   
    final static Object room = new Object();
    static boolean hasCigarette = false;


    public static void main(String[] args) {
   

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

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

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

        }, "送烟的").start();
    }
}

输出:

使用wait()/notify()方法的确解决了case one的问题,但是这样也是存在弊端的,例如如果存在多个等待者呢?

case three:引入多个等待者

public class TestWaitNotify {
   
    final static Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;


    public static void main(String[] args) {
   

        //小南
        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("可以开始干活了");
                }
            }
        }, "小南").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();
    }
}

输出:

出现了一种特殊情况,就是我送外卖的唤醒的room.notify();却是等烟的小南,这也是不正常的现象,出现该问题就是两个线程拥有了room对象并且进入了waitset,此时就不明确哪一个会被唤醒了。

但是如果使用notifyAll()方法去唤醒的话,会唤醒两个线程小南,小女,此时这样也不符合我们预期,我们只需要小女,不需要唤醒小南!

最终解决方法:

public class TestWaitNotify {
   
    final static Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;


    public static void main(String[] args) {
   

        //小南
        new Thread(() -> {
   
            synchronized (room) {
   
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
   
                    log.debug("没烟,先歇会!");
                    try {
   
                        room.wait();
                    } catch (InterruptedException e) {
   
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
   
                    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.notifyAll();
            }
        }, "送外卖的").start();
    }
}

输出:

将不去唤醒的线程在wait()外层用while()去处理,但满足某种条件说明改线程要去唤醒,此时while()就可以退出了,符合我们的预期。

synchronized(lock) {
   
 while(条件不成立) {
   
 lock.wait();
 }
 // 干活
}
//另一个线程
synchronized(lock) {
   
 lock.notifyAll();
}

学习资料:https://www.bilibili.com/video/BV16J411h7Rd?p=96