前一篇我们讲述了 同步锁 Lock,那么下面肯定就要讲解一下 同步锁 Lock 如何控制线程之间的通讯。
不过,在讲解 同步锁 Lock 通讯之间,我们首先来回顾一下 基本同步控制之间的线程声明周期,如下图:
image-20200822082951771
可以看到上面有很多通讯的方法. 其中最主要的就是以下的 wait() notify() notifyAll() 方法。这些就是控制线程间通讯的方法。
说明:
1. wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2. wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException异常
3. wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
现在我们来使用 生产者消费者的案例 来演示 虚假唤醒(notifyAll)的问题。
“生产者(
Productor
)将产品交给店员(Clerk
) 而消费者(Customer
)从店员(Clerk
)处取走产品 店员(Clerk
)一次只能持有固定数量的产品(比如:1) 如果生产者试图生产更多的产品,店员会叫生产者停一下 (wait) ,如果店中有空位放产品了再通知 (notifyAll) 生产者继续生产 如果店中没有产品了,店员会告诉消费者等一下 (wait),如果店中有产品了再通知 (notifyAll) 消费者来取走产品。 ”
// 店员类
class Clerk {
//定义商品的数量
private int product = 0; // 当前商品的数量
private int total = 10; // 货架允许存放的商品总数量
//进货的同步方法:提供给生产者线程调用
public synchronized void get() {
if (product > total) {
// 如果商品超出 货架允许存放商品的数量,则提示产品已摆满!
System.out.println("产品已摆满!");
} else {
// 如果还可以摆放产品,则继续摆放,设置产品的数量++,然后打印出来
System.out.println(Thread.currentThread().getName() + ":" + ++product);
}
}
//卖货的同步方法:提供给消费者线程调用
public synchronized void sale() {
if (product <= 0) {
//如果商品数量小于0,说明缺货了
System.out.println("缺货了");
} else {
//如果商品数量大于0,说明还有商品可以卖(商品的数量--)
System.out.println(Thread.currentThread().getName() + " :" + --product);
}
}
}
//生产者
class Productor implements Runnable {
//定义员工类对象
private Clerk clerk;
//构造器:接收传递的员工类对象
public Productor(Clerk clerk) {
this.clerk = clerk;
}
//生产者线程
@Override
public void run() {
//循环20次,调用员工收货,增加货物的数量
for (int i = 0; i < 20; i++) {
// 休眠200毫秒
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
//调用员工收货
clerk.get();
}
}
}
//消费者
class Consumer implements Runnable {
//定义员工类对象
private Clerk clerk;
//构造器:接收传递的员工类对象
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
//消费者线程
@Override
public void run() {
//循环20次,调用员工卖货,减少货物的数量
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
public class TestProductorAndConsumer {
public static void main(String[] args) {
//创建员工类对象
Clerk clerk = new Clerk();
//创建生产者线程类对象
Productor productor = new Productor(clerk);
//创建消费者线程类对象
Consumer consumer = new Consumer(clerk);
//创建线程
new Thread(productor, "生产者A").start();
new Thread(consumer, "消费者B").start();
}
}
执行测试如下:
image-20201103211614315
那么怎么去解决这个问题呢?
这个时候,就需要使用线程的通信 wait() 和 notifyAll() 的方法来处理了。处理思路如下:
image-20201103212511552
image-20201103212538344
image-20201103212926441
image-20201103213322161
image-20201103213544563
image-20201103214105426
其实根本的原因是线程在循环获取的时候,由于全部都走入了 if(product > total)
中,无法走到 else
分支唤醒线程,导致一直阻塞。
解决的办法很简单,只要去掉 else
分支即可,将唤醒的步骤放在最下方一定会执行的地方。
image-20201103214316925
image-20201103214402805
image-20201103214446056
上面的我们单个生产者,单个消费者执行是没有问题的。但是如果有多个生产者和消费者,将会出现 并发虚假唤醒的情况。
image-20201103215958829
这个为什么会出现这种情况呢?
这是因为线程的 notifyAll() 方法会将所有并发的 消费者 或者 生产者 线程进行唤醒,导致重复计算 产品的数量,从而导致数量的错误。
而这种同时将多个阻塞线程唤醒的情况,就是虚假唤醒。
在文档中搜索 object
类,查看 wait()
方法如下:
image-20201103220510583
image-20201103220601394
wait()
方法调用的地方,改为 while
循环image-20201103220704583
image-20201103220746383
image-20201103220804721
在生产与消费方法中,使用 while 解决了 虚假唤醒之后,下面来执行看看,如下:
image-20201103220928349
/**
* 生产者和消费者案例
*/
public class TestProductorAndConsumer {
public static void main(String[] args) {
//创建员工类对象
Clerk clerk = new Clerk();
//创建生产者线程类对象
Productor productor = new Productor(clerk);
//创建消费者线程类对象
Consumer consumer = new Consumer(clerk);
//创建线程
new Thread(productor, "生产者A").start();
new Thread(consumer, "消费者B").start();
new Thread(productor, "生产者c").start();
new Thread(consumer, "消费者D").start();
}
}
// 店员类
class Clerk {
//定义商品的数量
private int product = 0; // 当前商品的数量
private int total = 1; // 货架允许存放的商品总数量
//进货的同步方法:提供给生产者线程调用
public synchronized void get() {
while (product > total) {
// 如果商品超出 货架允许存放商品的数量,则提示产品已摆满!
System.out.println("产品已摆满!");
// 已经摆满货物,需要停止生产,设置线程阻塞
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//反之开始生产,唤醒线程执行操作
this.notifyAll();
// 如果还可以摆放产品,则继续摆放,设置产品的数量++,然后打印出来
System.out.println(Thread.currentThread().getName() + ":" + ++product);
}
//卖货的同步方法:提供给消费者线程调用
public synchronized void sale() {
while (product <= 0) {
//如果商品数量小于0,说明缺货了
System.out.println("缺货了");
//已经缺货,需要停止消费,设置线程阻塞
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有货物可以消费,唤醒线程
this.notifyAll();
//如果商品数量大于0,说明还有商品可以卖(商品的数量--)
System.out.println(Thread.currentThread().getName() + " :" + --product);
}
}
//生产者
class Productor implements Runnable {
//定义员工类对象
private Clerk clerk;
//构造器:接收传递的员工类对象
public Productor(Clerk clerk) {
this.clerk = clerk;
}
//生产者线程
@Override
public void run() {
//循环20次,调用员工收货,增加货物的数量
for (int i = 0; i < 20; i++) {
// 休眠200毫秒
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
//调用员工收货
clerk.get();
}
}
}
//消费者
class Consumer implements Runnable {
//定义员工类对象
private Clerk clerk;
//构造器:接收传递的员工类对象
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
//消费者线程
@Override
public void run() {
//循环20次,调用员工卖货,减少货物的数量
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
- Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的
功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版
本中的不同。
- 在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是 await、signal 和 signalAll。
- Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。
上面我们讲诉了生产者消费者的案例,在这种案例中我们采用的是 synchronized 同步方法来阻止线程安全问题的,如下:
image-20201103223435108
当然除了 同步方法,我们还可以使用锁 Lock 来阻止线程安全问题。下面我们将代码改写为使用 Lock 的实现方式。
image-20201103225302110
在改完了锁之后,我们可以看到线程通讯依然是使用 this.wait()
和 this.notifyAll()
,那么对于 lock
来说,有没有对应的 线程通讯 方法呢?
当然有,就是上面介绍的 Condition 通讯锁。
private final ReentrantLock lock = new ReentrantLock(); // 创建锁
private Condition condition = lock.newCondition(); // 通过lock获取condition
image-20201103225757510
image-20201103225929904
// 店员类
class Clerk {
//定义商品的数量
private int product = 0; // 当前商品的数量
private int total = 1; // 货架允许存放的商品总数量
private final ReentrantLock lock = new ReentrantLock(); // 创建锁
private Condition condition = lock.newCondition(); // 通过lock获取condition
//进货的同步方法:提供给生产者线程调用
public void get() {
//加锁
lock.lock();
try {
while (product > total) {
// 如果商品超出 货架允许存放商品的数量,则提示产品已摆满!
System.out.println("产品已摆满!");
// 已经摆满货物,需要停止生产,设置线程阻塞
try {
// this.wait();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//反之开始生产,唤醒线程执行操作
// this.notifyAll();
condition.signalAll();
// 如果还可以摆放产品,则继续摆放,设置产品的数量++,然后打印出来
System.out.println(Thread.currentThread().getName() + ":" + ++product);
} finally {
lock.unlock(); // 释放锁
}
}
//卖货的同步方法:提供给消费者线程调用
public void sale() {
lock.lock(); // 加锁
try {
while (product <= 0) {
//如果商品数量小于0,说明缺货了
System.out.println("缺货了");
//已经缺货,需要停止消费,设置线程阻塞
try {
// this.wait();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有货物可以消费,唤醒线程
// this.notifyAll();
condition.signalAll();
//如果商品数量大于0,说明还有商品可以卖(商品的数量--)
System.out.println(Thread.currentThread().getName() + " :" + --product);
} finally {
lock.unlock(); // 释放锁
}
}
}