什么是wait和notify
在java中我们可以使用「synchronized」解决线程同步的问题,通过该关键字能保证多线程并发情况下的线程安全。
package com.buydeem.share;
public class WaitNotifyDemo1 {
static final Object LOCK = new Object();
static Integer number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (LOCK){
number++;
System.out.printf("线程:[%s]打印number:[%d]%n",Thread.currentThread().getName(),number);
}
}
};
Thread t1 = new Thread(runnable);
t1.setName("t1");
Thread t2 = new Thread(runnable);
t2.setName("t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
例如上述的示例代码,我们通过synchronized关键字来使线程同步,最后的打印结果就是我们2000。(如果上述代码还不是很懂的话,建议不要浪费时间阅读本文,可以先去了解了synchronized之后再来看。关注公众号查看「多线程」专题。)
存在的问题
上面的示例中线程安全的问题解决了,但是还存在一个问题没有解决,那就是线程之间的协调问题。那什么是协调问题呢?假如说现在我增加一个需求,这个需求是不仅要保证线程安全,同时还需要让两个线程交替的打印出结果。想要完成这个任务,就必须要让线程之间通信,然后协调工作,这个就是前面协调的定义。
解决办法
针对上面的问题,我们可以使用wait和notify来解决。这里我先给出实现代码,后面我们再仔细分析。
public class WaitNotifyDemo2 {
static final Object LOCK = new Object();
static Integer number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (LOCK){
LOCK.notify();
number++;
System.out.printf("线程:[%s]打印number:[%d]%n",Thread.currentThread().getName(),number);
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t1 = new Thread(runnable);
t1.setName("t1");
Thread t2 = new Thread(runnable);
t2.setName("t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
修改代码如上所示,然后运行代码整个打印结果如下:
❝
线程:[t2]打印number:[1]
线程:[t1]打印number:[2]
线程:[t2]打印number:[3]
线程:[t1]打印number:[4]
……省略部分内容
线程:[t2]打印number:[1997]
线程:[t1]打印number:[1998]
线程:[t2]打印number:[1999]
线程:[t1]打印number:[2000]❞
其实实现代码并没大改,我们只是在增加了notify和wait两个方法而已,那下面我们就来细说这两个方法。
wait和notify
这两个方法被定义在Object中,从这一点我们可以看出,只要是java中的对象都会有这两个方法,因为Object类是所有类的父类。
wait
调用该方法,会导致调用该方法的线程进入等待。但是调用该方法前必须要获取到该对象的对象锁,否则将抛出「IllegalMonitorStateException」。同时还提供了两个支持指定等待时间的方法,可以指定等待时间。
notify
唤醒在此对象锁上等待的单个线程。同样该方法也需要先获取到对象锁,否则也将抛出「IllegalMonitorStateException」异常。同时还提供了一个唤醒在此对象锁上等待的所有线程notifyAll()。
为什么要先获取到对象锁
在对上面wait和notify介绍的过程中,我们强调了一点那就是必须要先获取到对象锁。这句话是什么意思呢?先看下面的例子:
public class WaitNotifyDemo3 {
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
LOCK.notify();
}
}
直接运行代码抛出下面的错误:
❝
Exception in thread “main” java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method) at com.buydeem.share.WaitNotifyDemo3.main(WaitNotifyDemo3.java:14)
❞
而我们所说的需要先获取对象锁指的是我们调用wait或者notify方法时,必须是在同步代码块中(即synchronized代码块),同时还必须是同一个对象。也就是说你同步对象是a(synchronized(a)),那么你调用的就应该是「a.wait或者a.notify」。
public class WaitNotifyDemo3 {
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
synchronized (LOCK){
LOCK.notify();
}
}
}
关于为什么这两个方法都要在同步代码块中调用,其实你可以这样想,线程调用对象的wait方法后,线程会立马释放持有的对象锁,而调用对象的notify同样该线程在同步代码块执行完成之后也要释放对象锁。这两个方法都需要释放对象锁,如果没有本身没有持有,那么哪来的释放呢?
wait和notify释放对象锁的时机
在上面我们讲过wait和notify的释放对象锁的时机不一样,但是没有细说。这里我们使用示例代码来验证我们上面说的。首先我们确定结论:
-
wait在对象调用完该方法之后释放对象锁 -
notify在该方法调用完之后并不会立马释放锁,而是需要退出同步代码块才会释放对象锁(有点废话,退出同步代码块当然就释放锁了)。
public class WaitNotifyDemo4 {
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (LOCK){
System.out.println("线程t1进入同步代码块获取到锁");
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程t1 wait后继续执行");
}
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (LOCK){
System.out.println("线程t2进入同步代码块");
System.out.println("线程t1的状态:"+t1.getState());
LOCK.notify();
System.out.println("线程t2已执行notify");
System.out.println("线程t1的状态:"+t1.getState());
}
}
});
t2.start();
t1.join();
t2.join();
}
}
上面的代码很简单,直接看运行结果:
❝
线程t1进入同步代码块获取到锁
线程t2进入同步代码块
线程t1的状态:WAITING
线程t2已执行notify
线程t1的状态:BLOCKED
线程t1 wait后继续执行❞
首先是线程t1先执行,进入同步代码块后调用wait释放了对象锁。接着就是线程t2开始执行,t2之所以能进入同步代码块是因为线程t1调用了wait释放了锁它才能拿到锁。接着t2打印出线程t1的状态为「WAITING」。接着线程t2调用了notify,然后打印出线程t1的状态为「BLOCKED」。此处说明了notify确实唤醒了线程t1(线程t1状态变化了),同时说明了线程t2并没有释放锁(线程t1的状态是BLOCKED,再次强调线程状态必须要了解清楚)。
死锁问题
关于什么是死锁这里就不说了,在使用wait和notify时我们需要牢记一点,调用wait只会释放当前对象上的锁,而不会把所有持有的锁都释放掉。如果不注意这一点,很容易造成死锁的发生。废话不多说,直接上代码。
public class WaitNotifyDemo5 {
static final Object LOCK1 = new Object();
static final Object LOCK2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK1) {
synchronized (LOCK2) {
try {
System.out.println("线程t1调用wait");
LOCK1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.setName("t1");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() ->{
synchronized (LOCK1){
synchronized (LOCK2){
System.out.println("线程t2执行");
}
}
});
t2.setName("t2");
t2.start();
t1.join();
t2.join();
}
}
上述代码中,线程t1先获取到LOCK1对象锁,接着再获取LOCK2的对象锁,最后调用LOCK1对象的wait方法。这个wait释放的是LOCK1上的对象锁,而线程t1持有的LOCK2对象锁并没有被释放。这就是前面我们说的wait只会释放该对象持有的锁而不是线程持有的所有锁。因为这一点直接导致了线程t2无法获取到LOCK1的对象锁,程序死锁了。我们可以通过jstack查看线程状态。
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode):
"t2" #14 prio=5 os_prio=0 tid=0x000001f320a57800 nid=0x24b4 waiting for monitor entry [0x000000c6553fe000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.buydeem.share.WaitNotifyDemo5.lambda$main$1(WaitNotifyDemo5.java:35)
- waiting to lock <0x00000000d6088aa8> (a java.lang.Object)
- locked <0x00000000d6088a98> (a java.lang.Object)
at com.buydeem.share.WaitNotifyDemo5$$Lambda$2/400136488.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1" #13 prio=5 os_prio=0 tid=0x000001f320a57000 nid=0x3490 in Object.wait() [0x000000c6552ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6088a98> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at com.buydeem.share.WaitNotifyDemo5.lambda$main$0(WaitNotifyDemo5.java:20)
- locked <0x00000000d6088aa8> (a java.lang.Object)
- locked <0x00000000d6088a98> (a java.lang.Object)
at com.buydeem.share.WaitNotifyDemo5$$Lambda$1/1879492184.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
从jstack打印的结果也印证了这一点。
经典模型-生产者和消费者
关于wait和notify最经典的问题就是生产者和消费者问题了。简单的说就是生产者负责一直产生数据,而消费者负责处理生产者的产出。
public class WaitNotifyDemo6 {
public static void main(String[] args) throws InterruptedException {
Store store = new Store(10, 0);
Thread product = new Thread(() -> {
while (true) {
try {
store.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread customer = new Thread(() -> {
while (true) {
try {
store.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
product.start();
customer.start();
product.join();
product.join();
}
}
class Store {
/**
* 仓库的最大容量
*/
private int maxCount;
/**
* 当前仓库的数量
*/
private int currentCount;
/**
* 创建一个仓库
*
* @param maxCount 最大仓库容量
* @param currentCount 初始化仓库数量
*/
public Store(int maxCount, int currentCount) {
this.maxCount = maxCount;
this.currentCount = currentCount;
}
/**
* 存储产品
*/
public synchronized void add() throws InterruptedException {
if (currentCount >= maxCount) {
wait();
}
System.out.printf("生产者:当前库存:[%d],增加后的库存为:[%d]%n", currentCount, ++currentCount);
notify();
}
/**
* 消费产品
*/
public synchronized void remove() throws InterruptedException {
if (currentCount <= 0) {
wait();
}
System.out.printf("消费者:当前库存:[%d],减掉的库存为:[%d]%n", currentCount, --currentCount);
notify();
}
}
上面的代码就是一个简单的生产者和消费者模型。代码比较简单,「Store」提供入库和出库的操作,然后生产者线程和消费者线程同时对仓库进行入库和出库操作。在一个生产者和一个消费者的情况下,并没有什么问题。
多生产者和多消费者问题
上面我们只是一个生产者和一个消费者的情况,现在我们改用成多个生产者和消费者的情况,核心代码没有做改变,只是增加了一个消费者和生产者。
public class WaitNotifyDemo7 {
public static void main(String[] args) throws InterruptedException {
Store2 store = new Store2(10, 0);
Thread p1 = new Thread(() -> {
while (true) {
try {
store.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
p1.setName("p1");
Thread p2 = new Thread(() -> {
while (true) {
try {
store.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
p2.setName("p2");
Thread c1 = new Thread(() -> {
while (true) {
try {
store.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
c1.setName("c1");
Thread c2 = new Thread(() -> {
while (true) {
try {
store.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
c2.setName("c2");
p1.start();
p2.start();
c1.start();
c2.start();
p1.join();
}
}
class Store2 {
/**
* 仓库的最大容量
*/
private int maxCount;
/**
* 当前仓库的数量
*/
private int currentCount;
/**
* 创建一个仓库
*
* @param maxCount 最大仓库容量
* @param currentCount 初始化仓库数量
*/
public Store2(int maxCount, int currentCount) {
this.maxCount = maxCount;
this.currentCount = currentCount;
}
/**
* 存储产品
*/
public synchronized void add() throws InterruptedException {
if (currentCount >= maxCount) {
wait();
}
System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
notify();
}
/**
* 消费产品
*/
public synchronized void remove() throws InterruptedException {
if (currentCount <= 0) {
wait();
}
System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
notify();
}
}
再次运行代码会发现,会出现过度消费和过度产出的问题。这是为什么呢?
修改if为while
核心的入库和出库代码如下:
/**
* 存储产品
*/
public synchronized void add() throws InterruptedException {
if (currentCount >= maxCount) {
wait();
}
System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
notify();
}
/**
* 消费产品
*/
public synchronized void remove() throws InterruptedException {
if (currentCount <= 0) {
wait();
}
System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
notify();
}
假设现在有一个生产者t1生产时(即调用了add方法),进行条件判断「currentCount >= maxCount」的值为「true」(即仓库满了),这时候线程t1将进入WAITING状态。如果此时线程t1被其他线程唤醒,线程t1则会执行后续的增加库存操作,而不会再去判断「currentCount >= maxCount」条件是否真的满足。此时和可能库存还是到了最大库存,这时候还往仓库添加产品,库存就超过了最大的库存。
所以对于该情况下的条件判断使用while替代if是一个更好的选择。我们修改入库和出库代码如下:
/**
* 存储产品
*/
public synchronized void add() throws InterruptedException {
// if (currentCount >= maxCount) {
// wait();
// }
while (currentCount >= maxCount){
wait();
}
System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
notify();
}
/**
* 消费产品
*/
public synchronized void remove() throws InterruptedException {
// if (currentCount <= 0) {
// wait();
// }
while (currentCount <= 0){
wait();
}
System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
notify();
}
我们使用while替代了if,再次运行代码并未出现库存超出或者库存为负的情况。
notifyAll的妙用
上面的代码好像是没有什么问题了,现在我修改最大仓库数量。假设我的仓库最大数量为1,我们再次运行代码。最后的结果是,程序正常运行了一小会儿之后,然后控制台不再打印任何信息。使用jstack工具查看应用的状态如下:
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode):
"c2" #16 prio=5 os_prio=0 tid=0x000001f6cd2d5000 nid=0x2bec in Object.wait() [0x00000086188fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
at java.lang.Object.wait(Object.java:502)
at com.buydeem.share.Store2.remove(WaitNotifyDemo7.java:111)
- locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
at com.buydeem.share.WaitNotifyDemo7.lambda$main$3(WaitNotifyDemo7.java:50)
at com.buydeem.share.WaitNotifyDemo7$$Lambda$4/400136488.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"c1" #15 prio=5 os_prio=0 tid=0x000001f6cd2ce000 nid=0x1d20 in Object.wait() [0x00000086187ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
at java.lang.Object.wait(Object.java:502)
at com.buydeem.share.Store2.remove(WaitNotifyDemo7.java:111)
- locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
at com.buydeem.share.WaitNotifyDemo7.lambda$main$2(WaitNotifyDemo7.java:39)
at com.buydeem.share.WaitNotifyDemo7$$Lambda$3/984213526.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"p2" #14 prio=5 os_prio=0 tid=0x000001f6cd2cd000 nid=0xfc4 in Object.wait() [0x00000086186fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
at java.lang.Object.wait(Object.java:502)
at com.buydeem.share.Store2.add(WaitNotifyDemo7.java:97)
- locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
at com.buydeem.share.WaitNotifyDemo7.lambda$main$1(WaitNotifyDemo7.java:28)
at com.buydeem.share.WaitNotifyDemo7$$Lambda$2/2094777811.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"p1" #13 prio=5 os_prio=0 tid=0x000001f6cd2cc800 nid=0x3228 in Object.wait() [0x00000086185ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
at java.lang.Object.wait(Object.java:502)
at com.buydeem.share.Store2.add(WaitNotifyDemo7.java:97)
- locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
at com.buydeem.share.WaitNotifyDemo7.lambda$main$0(WaitNotifyDemo7.java:17)
at com.buydeem.share.WaitNotifyDemo7$$Lambda$1/1879492184.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
从结果可以看出,所有的线程都处于wait状态。这是为什么呢?现在想象这么一个场景
-
线程c1获取到锁 -
线程c2,p1,p2都处于wait状态 -
当前库存是0
现在线程c1判断库存为0,那么线程c1入库完成之后(库存改为1)调用notify。notify唤醒的不是消费者而是生产者c2,生产者c2判断「currentCount >= maxCount」调用wait。至此所有线程处于waiting状态,生产者不能生产产品,消费者也无法消费产品。
通过上面的分析,我们知道了问题的核心在于「notify」是不能唤醒指定的线程的。这将导致一个问题就是当需要唤醒消费者消费时,我们和可能唤醒的不是消费者。这时候我们可以使用「notifyAll」来唤醒所有等待在同一个对象锁上的所有线程,这样不符合条件的线程再次调用wait进入waiting状态,而符合执行条件的线程则能正常执行。
/**
* 存储产品
*/
public synchronized void add() throws InterruptedException {
// if (currentCount >= maxCount) {
// wait();
// }
while (currentCount >= maxCount){
wait();
}
System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
//notify();
notifyAll();
}
/**
* 消费产品
*/
public synchronized void remove() throws InterruptedException {
// if (currentCount <= 0) {
// wait();
// }
while (currentCount <= 0){
wait();
}
System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
// notify();
notifyAll();
}
}
小结
通过上文我们知道了解了wait和notify的作用,它主要解决了synchronized同步代码块之间线程的协调问题。
同样它也不是那么完美,它并不是适用于Lock。例如如果我们使用ReentrantLock时,我们并不能通过wait和notify来实现线程间的协调。同样notify和notifyAll存在无法唤醒指定线程的问题。对于Lock情况下如何解决此类问题,关注公众号,后续我会更新到这部分内容。
同时我们还在文章了说了经典的生产者和消费者模型,以及实现过程中存在的问题,还有问题分析以及解决。
-
工程代码示例: https://gitee.com/zengchao_workspace/wait_notify
原文始发于微信公众号(一只菜鸟程序员):wait和notify
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/73046.html