生产者消费者模型中存在的死锁风险及解决方法
前言:
在笔者的另一篇文章《Java多线程之浅谈死锁问题》一文中,笔者在没有用到wait和notify方法的情况下演示了由于共享资源的占用冲突而导致的线程死锁问题.
而本文中笔者会演示线程通信时产生的死锁风险,即生产者消费者模型使用wait,notify会造成的死锁风险及解决方法.
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
代码演示及分析:
商品类(Clerk):
设置商品数量上限和编号(为了使死锁出现的几率大大增加,我们在此将商品的最大数量设为极小的1.
//商品类
class Clerk{
int productCount=0;//商品编号
int maxCount=1;//商品的最大数量
}
生产者类(Producer)
class Producer extends Thread{
Clerk clerk;
public Producer(Clerk clerk) {
this.clerk=clerk;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(clerk) {
while(true) {
while(clerk.productCount==clerk.maxCount) {
//等待
try {
clerk.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
clerk.productCount++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+clerk.productCount+"个商品");
clerk.notify();
}
}
}
消费者类(Customer)
class Customer extends Thread{
Clerk clerk;
public Customer(Clerk clerk) {
this.clerk=clerk;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(clerk) {
while(true) {
while(clerk.productCount==0) {
try {
clerk.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"开始消费第"+clerk.productCount+"个商品");
clerk.productCount--;
clerk.notify();
}
}
}
}
程序main方法
public static void main(String[] args) {
DeadLock2 dlock=new DeadLock2();
Clerk clerk=dlock.new Clerk();
Producer p1=dlock.new Producer(clerk);
Customer c1=dlock.new Customer(clerk);
//Customer c2=dlock.new Customer(clerk);
p1.setName("生产者1");
c1.setName("消费者1");
//c2.setName("消费者2");
p1.start();
c1.start();
//c2.start();
}
当我们使用单生产者-单消费者模式进行生产时,我们会发现程序跑的相当畅快.运行结果如下:
但是当我们造出第二个消费者,实行两消费者-单生产者时,程序却莫名其妙出现了锁死,结果如下:
那么为什么一对一时程序走的顺利,到多对一时便出现了死锁了呢?
这其实是跟我们使用的notify有关系.
原因如下种情况:
- 最开始商品数量为0,生产者1拿到锁,开始生产.
- 当生产者1在生产了一个商品之后,此时发现最大的商品数量已为1,便会调用wait()方法进入了Clerk对象的等待池中等待被唤醒(进入等待池之后便不会进行锁的争抢,只有被唤醒后才能进入锁池进行锁的竞争).
- 消费者1便将其消费,此时商品数量为零,消费者1进入等待池,调用notify方法随机唤醒等待池中的一个线程进入锁池.
- 如果唤醒的是生产者1线程,那么生产1继续执行,程序继续
- 但如果唤醒的是生产者2线程,生产者2线程发现商品数量为0,则会直接进入等待池,此时三个线程全在等待池中沉睡,产生了死锁.
全部代码:
package DeadLock;
public class DeadLock2 {
public static void main(String[] args) {
DeadLock2 dlock=new DeadLock2();
Clerk clerk=dlock.new Clerk();
Producer p1=dlock.new Producer(clerk);
Customer c1=dlock.new Customer(clerk);
Customer c2=dlock.new Customer(clerk);
p1.setName("生产者1");
c1.setName("消费者1");
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
//商品类
class Clerk{
int productCount=0;//商品的最大数量
int maxCount=1;//商品的最大数量
}
/*
*生产者
*
*/
class Producer extends Thread{
Clerk clerk;
public Producer(Clerk clerk) {
this.clerk=clerk;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(clerk) {
while(true) {
while(clerk.productCount==clerk.maxCount) {
//等待
try {
clerk.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
clerk.productCount++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+clerk.productCount+"个商品");
clerk.notify();
}
}
}
}
class Customer extends Thread{
Clerk clerk;
public Customer(Clerk clerk) {
this.clerk=clerk;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(clerk) {
while(true) {
while(clerk.productCount==0) {
try {
clerk.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"开始消费第"+clerk.productCount+"个商品");
clerk.productCount--;
clerk.notify();
}
}
}
}
}
解决办法:
破坏“不抢占”条件使用
使用notifyAll代替notify方法.
notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。
notify调用后,只会将等待池中的一个随机线程移到锁池。
notifyAll方法可以将全部线程唤醒,重新进行新一轮的争抢,使用notifyAll不再会出现死锁.
使用notifyAll后运行结果: