目录
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来实现。