内置锁(二)synchronized下的等待通知机制
一、等待/通知机制的简介
线程之间的协作:
为了完成某个任务,线程之间需要进行协作,采取的方式:中断、互斥,以及互斥上面的线程的挂起、唤醒;如:生成者--消费者模式、或者某个动作完成,可以唤醒下一个线程、管道流已准备等等;
等待/通知机制:
等待/通知机制 是线程之间的协作一种常用的方式之一,在显示锁Lock 和 内置锁synchronized都有对应的实现方式。
等待/通知机制 经典的使用方式,便是在生产者与消费者的模式中使用:
1、生产者负责生产商品,并送到仓库中存储;
2、消费者则从仓库中获取商品,享受商品;
3、仓库的容量有限,当仓库满了后,生产者就会停止生产,进入等待状态。直到消费者消耗了一部分商品,通知生产者,仓库有空位了,生产者才会继续生产商品;
4、当消费者的消耗速度过快,消耗光仓库的商品时,也会停止消耗,进入等待状态,直到生产者生产了商品,并通知唤醒消费者时,消费者才继续消耗商品。
二、等待/通知机制 在synchronized下的实现:
在内置锁下,等待/通知机制 是依赖于wait( )和 notify、notifyAll 来实现的,这三个方法都是继承自根类Object中。
下面是这几个方法的API描述,wait()方法有3个版本:
void wait():
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void wait(long timeout):
超时等待,在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。如果时间当时间到来了,线程还是没有被唤醒或中断,那么就会自动唤醒;
void wait(long timeout, int nanos):
参数timeout的单位是毫秒,参数nanos的单位是纳秒,即等待时间精确到纳秒。其他特性与wait(long timeout)一样。
void notify():
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
void notifyAll():
唤醒在此对象监视器上等待的所有线程。
1、调用 wait、notify、notifyAll 前,必须先获取锁
在调用 wait、notify、notifyAll 前,必须获取锁(对象监视器),而且这三个方法是由synchronized的对象锁(对象监视器)来调用。对象监视器 中维护着两个队列:就绪队列,等待队列。调用了wait()方法就是使线程进入等待队列,不再参与锁的竞争,直到被唤醒才能重新进入就绪队列,才可能获取锁,再次被执行。
如果在没有获取锁(对象监视器)的情况下,将会抛出IllegalMonitorStateException 异常:
wait、notify、notifyAll 方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
- 通过执行此对象的同步实例方法。
- 通过执行在此对象上进行同步的 synchronized 语句的正文。
- 对于 Class 类型的对象,可以通过执行该类的同步静态方法。
一次只能有一个线程拥有对象的监视器。
抛出:
IllegalMonitorStateException - 如果当前线程不是此对象监视器的所有者。
看下面的例子,消费者想要10个以上的商品,如果不够,则等待生产者生产足够的商品,生产者再来唤醒消费者线程。
//生产者与消费者互斥使用仓库
public static List<String> warehouse = new LinkedList<>();
public static void main(String[] args) {
//生产者线程
Thread thread_1 = new Thread(){
@Override
public void run() {
//对象监视器为warehouse,必须先获取这个对象监视器
synchronized ( warehouse) {
int i = 0;
//生产10个商品
while(warehouse.size()<=10){
++i;
//生产商品,添加进仓库
warehouse.add("生产了商品goods"+i);
//当商品数量足够时,便唤醒消费者线程
if(warehouse.size()>=10){
warehouse.notify();
//生产任务完成,跳出循环,结束运行,从而可以释放锁
break;
}
}
}
}
};
//消费者线程
Thread thread_2 = new Thread(){
@Override
public void run() {
synchronized (warehouse) {
try {//如果仓库的商品数量不能满足消费者
if(warehouse.size()<10){
//消费者进入等待队列,等待被唤醒
warehouse.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//消费商品
for(String goods:warehouse){
System.out.println("消费者消费了商品:"+goods);
}
}
}
};
//
thread_2.start();
thread_2.setPriority(Thread.MAX_PRIORITY);
thread_1.start();
}
运行结果:
消费者消费了商品:商品goods1
消费者消费了商品:商品goods2
消费者消费了商品:商品goods3
消费者消费了商品:商品goods4
消费者消费了商品:商品goods5
消费者消费了商品:商品goods6
消费者消费了商品:商品goods7
消费者消费了商品:商品goods8
消费者消费了商品:商品goods9
消费者消费了商品:商品goods10
2、wait()方法是可以被中断的
如果线程在等待过程中被中断,那么线程的等待状态就会被打断,即由对象监视器的等待队列进入就绪队列;所以,可以总结一下,线程在等待状态被唤醒的情况:
- 被其他线程调用notify、notifyAll方法唤醒;
- 如果是超时等待,那么时间到来了,线程也会自动唤醒。
- 有中断发生,线程的等待状态被打断,强制唤醒。
public static void main(String[] args) throws InterruptedException {
final String lock = "lock";
Thread thread = new Thread(){
@Override
public void run() {
synchronized (lock) {
System.out.println("我即将进入等待状态!");
try {
//超时等待5秒
lock.wait(5000);
} catch (InterruptedException e) {
System.out.println("啊!我被中断了");
e.printStackTrace();
}
System.out.println("我在运行中....");
}
}
};
thread.start();
//main线程睡眠1秒后,中断线程thread
Thread.sleep(1000);
thread.interrupt();
}
运行结果:
线程thread因为中断,还没等待5秒,就被提前唤醒。
3、wait()是释放锁,notify、notifyAll 不释放锁
调用wait()方法是线程马上释放锁,进入等待队列,而调用notify、notifyAll 后,线程不会释放锁,而仅仅唤醒线程而已,要等待当前运行完后,锁才是真的被释放,被唤醒的线程才可以竞争获取锁。
final String lock = "lock";
Thread thread_1 = new Thread("thread_1"){
@Override
public void run() {
synchronized (lock) {
System.out.println(getName()+"进入等待状态");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"被唤醒,继续运行...");
}
}
};
thread_1.start();
Thread thread_2 = new Thread("thread_2"){
@Override
public void run() {
synchronized (lock) {
System.out.println(getName()+"在运行...");
try {
//模拟占用着锁,运行了一秒,sleep在睡眠中是不会释放锁的
Thread.sleep(1000);
//在调用notifyAll方法前,线程1的状态
System.out.println("线程"+thread_1.getName()+"被唤醒前的状态是:"+thread_1.getState());
//唤醒在对象监视器的等待队列的所有线程
lock.notifyAll();
//模拟占用着锁,继续运行5次,约为5秒
int i = 0;
while(i<5){
System.out.println("线程"+thread_1.getName()+"的状态是:"+thread_1.getState());
sleep(1000);
i++;
}
System.out.println("线程"+getName()+"运行结束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread_2.start();
运行结果:
thread_1进入等待状态
thread_2在运行...
线程thread_1被唤醒前的状态是:WAITING
线程thread_1的状态是:BLOCKED
线程thread_1的状态是:BLOCKED
线程thread_1的状态是:BLOCKED
线程thread_1的状态是:BLOCKED
线程thread_1的状态是:BLOCKED
线程thread_2运行结束!
thread_1被唤醒,继续运行...
三、易混淆的几个方法的区别
1、wait( )方法:
- 先释放锁,再进入等待状态;
- 使当前线程暂停运行,让出CPU,不再参与CPU的调度,直到被唤醒为止;
2、sleep()方法:
- 在睡眠过程中,不释放锁;
- 使当前线程暂停运行,让出CPU,不再参与CPU的调度,直到睡眠时间到来;
3、yield( )方法:
- 不释放锁
- 使当前线程暂停运行,给同等优先级或高优先级的线程让出CPU。但有可能让出CPU失败,因为调用yield( )方法后,当前线程由运行状态,变成就绪状态,会马上参与CPU的调度,也就说可能再次被调度,当前线程依旧占用着CPU。