在Java的Object类中有2个我们不怎么常用(框架中用的更多)的方法:wait()与notify()或notfiyAll(),这两个方法主要用于多线程间的协同处理,即控制线程之间的等待、通知、切换及唤醒。
API 介绍
obj.wait()
让进入 object 监视器的线程到 waitSet 等待
obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒
底层原理:
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,需要进入 EntryList 重新竞争
代码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public class WaitNotify {
public static void main(String[] args) {
Object lock = new Object(); new Thread(() -> {
System.out.println("thread1 is ready");
try { Thread.sleep(2000); } catch (InterruptedException e) {
} synchronized (lock) { lock.notify(); System.out.println("thread1 is notify,but not exit synchronized"); System.out.println("thread1 is exit synchronized"); }
}).start();
new Thread(() -> { System.out.println("thread2 is ready");
try { Thread.sleep(1000); } catch (InterruptedException e) {
} synchronized (lock) { try { System.out.println("thread2 is waiting"); lock.wait(); System.out.println("thread2 is awake"); } catch (InterruptedException e) { } } }).start(); } }
|
上面的代码运行结果如下
1 2 3 4 5 6
| thread1 is ready thread2 is ready thread2 is waiting thread1 is notify,but not exit synchronized thread 1 is exit synchronized thread2 is awake
|
看到这里你会发现平平无奇,好像也没什么特殊的事情,但是如果深入分析下,就会发现以下的问题
- 为何调用wait或者notify一定要加synchronized,不加行不行?
- 调用notify/notifyAll后等待中的线程会立刻唤醒吗?
- 调用notify/notifyAll是随机从等待线程队列中取一个或者按某种规律取一个来执行?
- wait的线程是否会影响系统性能?
针对上面的问题,我们逐个分析下
为何调用wait或者notify一定要加synchronized,不加行不行?
如果你不加,你会得到下面的异常
1
| Exception in thread "main" java.lang.IllegalMonitorStateException
|
在JVM源代码中首先会检查当前线程是否持有锁,如果没有持有则抛出异常
其次为什么要加,也有比较广泛的讨论,首先wait/notify是为了线程间通信的,为了这个通信过程不被打断,需要保证wait/notify这个整体代码块的原子性,所以需要通过synchronized来加锁。
调用notify/notifyAll后等待中的线程会立刻唤醒吗?
hotspot真正的实现是退出同步代码块的时候才会去真正唤醒对应的线程,不过这个也是个默认策略,也可以改的,在notify之后立马唤醒相关线程。 这个也可从jdk源代码的objectMonitor类objectMonitor::notify方法中看到.在调用notify时的默认策略是Policy == 2(这个值是源码中的初值,可以通过-XX:SyncKnobs来设置)
其实对于Policy(1、2、3、4)都是将objectMonitor的ObjectWaiter集合中取出一个等待线程,放入到_EntryList(blocked线程集合,可以参与下次抢锁),只是放入_EntryList的策略不一样,体现为唤醒wait线程的规则不一样。
对于默认策略notify在将一个等待线程放入阻塞线程集合之后就退出,因为同步块还没有执行完monitorexit,锁其实还未释放,所以在打印出“thread1 is exit synchronized!”的时候,thread2线程还是blocked状态(因为thread1还没有退出同步块)。
这里可以发现,对于不在Policy中的情况,会直接将一个ObjectWaiter进行unpark唤醒操作,但是被唤醒的线程是否立即获取到了锁呢?答案是否定的。
调用notify/notifyAll是随机从等待线程队列中取一个或者按某种规律取一个来执行?
我们自己实现可能一个for循环就搞定了,不过在jvm里实现没这么简单,而是借助了monitor_exit,上面我提到了当某个线程从wait状态恢复出来的时候,要先获取锁,然后再退出同步块,所以notifyAll的实现是调用notify的线程在退出其同步块的时候唤醒起最后一个进入wait状态的线程,然后这个线程退出同步块的时候继续唤醒其倒数第二个进入wait状态的线程,依次类推,同样这是一个策略的问题,jvm里提供了挨个直接唤醒线程的参数,这里要分情况:
- 如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来
- 如果是通过nootifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即LIFO的策略
wait的线程是否会影响系统性能?
这个或许是大家比较关心的话题,因为关乎系统性能问题,wait/nofity是通过jvm里的park/unpark机制来实现的,在linux下这种机制又是通过pthread_cond_wait/pthread_cond_signal来玩的,因此当线程进入到wait状态的时候其实是会放弃cpu的,也就是说这类线程是不会占用cpu资源,也不会影响系统加载。
什么是监视器(monitor)
Java中每一个对象都可以成为一个监视器(Monitor), 该Monitor由一个锁(lock), 一个等待队列(waiting queue ), 一个入口队列( entry queue)组成. 对于一个对象的方法, 如果没有synchonized关键字, 该方法可以被任意数量的线程,在任意时刻调用。 对于添加了synchronized关键字的方法,任意时刻只能被唯一的一个获得了对象实例锁的线程调用。
进入区(Entry Set): 表示线程通过 synchronized要求获得对象锁,如果获取到了,则成为拥有者,如果没有获取到在在进入区等待,直到其他线程释放锁之后再去竞争(谁获取到则根据)
拥有者(Owner): 表示线程获取到了对象锁,可以执行synchronized包围的代码了
等待区(Wait Set): 表示线程调用了wait方法,此时释放了持有的对象锁,等待被唤醒(谁被唤醒取得监视器锁由jvm决定)
关于sleep
它是一个静态方法,一般的调用方式是Thread.sleep(2000),表示让当前线程休眠2000ms,并不会让出监视器,这一点需要注意。
对比 sleep():
- 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信
- 对锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
- 使用区域不同:wait() 方法必须放在同步控制方法和同步代码块(先获取锁)中使用,sleep() 方法则可以放在任何地方使用