Java 多线程—生产者和消费者问题以及死锁

Java 多线程—生产者和消费者问题以及死锁_第1张图片

作者:金良([email protected]) csdn博客:http://blog.csdn.net/u012176591

一、生产者和消费者模型

模型简述

“生产者消费者模型”,准确说应该是“生产者-消费者-仓储”三元模型。
对于此模型,应该明确一下几点:
1、生产者仅仅在现有仓储量加上该生产者要生产的产品量的和小于仓库容量时才能生产,否则只能等待仓库中部分产品被消费后才能生产产品。

2、消费者仅仅在仓储有足够仓储量的时候才能消费,仓储量小于要消费的量则等待。

3、仓库的产品量更新(分消费和生产两种情况,这两种情况都会改变仓储量)后,应该通知等待的消费者去消费。

完整代码及执行结果

package com.xujinliang.producer_consumer;


/**
 * 测试程序
 * @author jin
 *
 */

public class Test {
	public static void main(String[] args) {
		Inventory inventory = new Inventory(0,54); 
        Consumer c1 = new Consumer(39, inventory); 
        Consumer c2 = new Consumer(20, inventory); 
        Consumer c3 = new Consumer(30, inventory); 
        
        Producer p1 = new Producer(11, inventory); 
        Producer p2 = new Producer(12, inventory); 
        Producer p3 = new Producer(13, inventory); 
        Producer p4 = new Producer(14, inventory); 
        Producer p5 = new Producer(15, inventory); 
        Producer p6 = new Producer(16, inventory); 
        Producer p7 = new Producer(9 , inventory); 
        Producer p8 = new Producer(13, inventory);

        c1.start(); 
        c2.start(); 
        c3.start(); 
        
        p1.start(); 
        p2.start(); 
        p3.start(); 
        p4.start(); 
        p5.start(); 
        p6.start(); 
        p7.start(); 
        p8.start();
            
    } 
} 


package com.xujinliang.producer_consumer;


/**
 * 仓库
 * @author jin
 *
 */
class Inventory { 
    //public static final int max_size = 40; //最大库存量 
    public int remaining;     //当前库存量 
    public int max_size;

    Inventory(int remaining,int max_size) { 
    	this.max_size = max_size;
    	this.remaining = remaining; 
    } 
    /** 
     * 生产指定数量的产品 
     * 
     * @param neednum 
     */ 
    public synchronized void produce(int neednum) { 
    	//测试是否需要生产 
        while (neednum + remaining > max_size) { 
        	System.out.println(Thread.currentThread().getName()
        			+": 超量:要生产的产品数量" + neednum + "超过仓库余量" + (max_size - remaining)); 
            try { 
            	//当前的生产线程等待 
            	wait();
            } catch (InterruptedException e) {
            	e.printStackTrace(); 
            } 
        } 
        //满足生产条件,则进行生产,这里简单的更改当前库存量 
        remaining += neednum; 
        System.out.println(Thread.currentThread().getName()
        		+": 已经生产了" + neednum + "个产品,现仓储量为" + remaining); 
        //唤醒在此对象监视器上等待的所有线程 
        notifyAll(); 
    } 


    /** 
     * 消费指定数量的产品 
     * 
     * @param neednum 
     */ 
    public synchronized void consume(int neednum) {
    	//测试是否可消费 
        while (remaining < neednum) {
        	System.out.println(Thread.currentThread().getName()
        			+": 不足:要消费的数量" + neednum + "大于库存量" + remaining);
            try {
            	//当前的生产线程等待 
                wait(); 
            } catch (InterruptedException e) { 
            	e.printStackTrace(); 
            } 
        } 
        //满足消费条件,则进行消费,这里简单的更改当前库存量 
        remaining -= neednum; 
        System.out.println(Thread.currentThread().getName()
        		+": 已经消费了" + neednum + "个产品,现仓储量为" + remaining); 
        //唤醒在此对象监视器上等待的所有线程 
        notifyAll(); 
    } 
}


package com.xujinliang.producer_consumer;


/**
 * 生产者
 * @author jin
 *
 */
class Producer extends Thread { 
    private int neednum;                //生产产品的数量 
    private Inventory inventory;            //仓库 

    Producer(int neednum, Inventory inventory) {
    	this.neednum = neednum; 
    	this.inventory = inventory; 
    } 
    public void run() { 
    	System.out.println(Thread.currentThread().getName()+"启动");
        //生产指定数量的产品 
    	inventory.produce(neednum); 
    } 
}

package com.xujinliang.producer_consumer;
/**
 * 消费者
 * @author jin
 *
 */

class Consumer extends Thread {
	private int neednum;                //生产产品的数量 
    private Inventory inventory;            //仓库 

    Consumer(int neednum, Inventory inventory) {
    	this.neednum = neednum; 
    	this.inventory = inventory; 
    } 
    public void run() { 
    	System.out.println(Thread.currentThread().getName()+"启动");//消费指定数量的产品 
        inventory.consume(neednum); 
    } 
}
转自博客: http://blog.csdn.net/golden1314521/article/details/24174461

运行结果:

Thread-0启动
Thread-2启动
Thread-0: 不足:要消费的数量39大于库存量0
Thread-2: 不足:要消费的数量30大于库存量0
Thread-1启动
Thread-1: 不足:要消费的数量20大于库存量0
Thread-3启动
Thread-3: 已经生产了11个产品,现仓储量为11
Thread-1: 不足:要消费的数量20大于库存量11
Thread-2: 不足:要消费的数量30大于库存量11
Thread-0: 不足:要消费的数量39大于库存量11
Thread-6启动
Thread-6: 已经生产了14个产品,现仓储量为25
Thread-0: 不足:要消费的数量39大于库存量25
Thread-4启动
Thread-2: 不足:要消费的数量30大于库存量25
Thread-7启动
Thread-7: 已经生产了15个产品,现仓储量为40
Thread-1: 已经消费了20个产品,现仓储量为20
Thread-5启动
Thread-8启动
Thread-2: 不足:要消费的数量30大于库存量20
Thread-10启动
Thread-0: 不足:要消费的数量39大于库存量20
Thread-9启动
Thread-4: 已经生产了12个产品,现仓储量为32
Thread-0: 不足:要消费的数量39大于库存量32
Thread-2: 已经消费了30个产品,现仓储量为2
Thread-9: 已经生产了9个产品,现仓储量为11
Thread-10: 已经生产了13个产品,现仓储量为24
Thread-8: 已经生产了16个产品,现仓储量为40
Thread-5: 已经生产了13个产品,现仓储量为53
Thread-0: 已经消费了39个产品,现仓储量为14

程序存在的问题:

看上边代码第4行,也就是仓库inventory初始化那一行,其第二个参数表示仓库最大容量,需要提到的是,仓库最大容量的设置要小心,否则程序执行可能会出问题。
举例如下:
代码改动:仅把最大容量更改为40(Inventory inventory = new Inventory(0,40)),其余代码均不变,继续执行,某次执行结果如下:
Thread-2启动
Thread-2: 不足:要消费的数量30大于库存量0
Thread-3启动
Thread-3: 已经生产了11个产品,现仓储量为11
Thread-6启动
Thread-6: 已经生产了14个产品,现仓储量为25
Thread-7启动
Thread-2: 不足:要消费的数量30大于库存量25
Thread-7: 已经生产了15个产品,现仓储量为40
Thread-2: 已经消费了30个产品,现仓储量为10
Thread-10启动
Thread-10: 已经生产了13个产品,现仓储量为23
Thread-0启动
Thread-0: 不足:要消费的数量39大于库存量23
Thread-1启动
Thread-1: 已经消费了20个产品,现仓储量为3
Thread-5启动
Thread-5: 已经生产了13个产品,现仓储量为16
Thread-4启动
Thread-0: 不足:要消费的数量39大于库存量16
Thread-4: 已经生产了12个产品,现仓储量为28
Thread-8启动
Thread-8: 超量:要生产的产品数量16超过仓库余量12
Thread-0: 不足:要消费的数量39大于库存量28
Thread-9启动
Thread-9: 已经生产了9个产品,现仓储量为37
Thread-0: 不足:要消费的数量39大于库存量37
Thread-8: 超量:要生产的产品数量16超过仓库余量3
可以看出程序最后卡死了。
为什么会卡死呢?
看27行,仓储量为37,我们设置的仓库容量为40,故还可以往仓库里放置3个产品。需要提到的是,此时仅剩下Thread-0和Thread-8等待执行,其余线程都已经执行结束了。继续看第28行,Thread-0要消费的数量39大于库存量37,故只能等待。
看29行,Thread-8要生产的产品数量16超过仓库余量3,也只能等待。
所以就形成了这样一种情况: 总消费量小于总生产量,但是既不能生产,又不能消费,程序卡在这儿不能往下执行。

解决方案1:

        设单一线程消费量的最大值是Cm,最大生产量是Pm,仓库容量M,我们就是要确定合适的M取值,以避免上述卡死现象。
        考虑最差的情况:
        当前仓储量是Cm-1,而且仅剩下消费量Cm和生产量是Pm的两个线程处于等待状态,那么此时只能生产,所以仓库容量M=Cm-1+Pm,这个值是程序 不发生卡死的最小取值
        最初的程序中inventory容量初始化为54,就是这个原因。
       

解决方案2:

根据消费驱动生产,同时生产兼顾仓库,如果仓不满就生产,并对每次最大消费量做个限制,这样就不存在此问题了。

二、并发协作-死锁

代码及解析

发生死锁的原因一般是两个对象的锁相互等待造成的。
这里给出一个 完整的会形成死锁的例子并有详细解析。
package com.xujinliang.deadlock;
/**
 * Java线程、并发和死锁
 * @author jin
 *
 */

public class Test {
	public static void main(String[] args) {
		DeadLock deadlock = new DeadLock();
		
	MyThread t1 = new MyThread(deadlock); 
        MyThread t2 = new MyThread(deadlock); 
        MyThread t3 = new MyThread(deadlock); 
        MyThread t4 = new MyThread(deadlock); 

        t1.start(); 
        t2.start(); 
        t3.start(); 
        t4.start(); 
    } 
}


package com.xujinliang.deadlock;
/**
 * 
 * @author jin
 *
 */

class MyThread extends Thread { 
    private DeadLock deadlock; 


    MyThread(DeadLock deadlock) { 
            this.deadlock = deadlock; 
    } 


    @Override 
    public void run() { 
            deadlock.read(); 
            deadlock.write();
    } 
} 


package com.xujinliang.deadlock;
/**
 * 
 * @author jin
 *
 */


class DeadLock {
	
	private class Resource {//内部私有类
    } 

	//内部私有对象
    private Resource resourceA = new Resource(); 
    private Resource resourceB = new Resource(); 

    public void read() {
    	synchronized (resourceA) {
    		System.out.println("read() :" + Thread.currentThread().getName() 
    				+ "获取了resourceA的锁!"); 
            synchronized (resourceB) {
            	System.out.println("read() :" + Thread.currentThread().getName() 
            			+ "获取了resourceB的锁!"); 
            } 
        } 
    } 
	//write()方法与 read() 的很重要的区别是:write()方法把resourceB的锁的代码放在了resourceA之前,这是它与read()方法最重要的区别,也是本程序能够形成死锁的根源。
    public void write() {
	//
    	synchronized (resourceB) { 
    		System.out.println("write():" + Thread.currentThread().getName() 
    				+ "获取了resourceB的锁!"); 
            synchronized (resourceA) {
            	System.out.println("write():"+ Thread.currentThread().getName() 
            			+ "获取了resourceA的锁!"); 
            } 
        } 
    }
}

下面是程序执行的几个显示结果,大多数情况下它们都发生了死锁:

  执行结果1:

分析:
第3行,Thread-2进入方法write()并获取了resourceB的锁;
    第4行,Thread-0进入方法read()并获取了resourceA的锁;
                 Thread-2要继续执行,比需得到resourceA的锁;但resourceA的锁被Thread-0占据,Thread-0 要获得resourceB的锁才能继续执行进而释放resourceA的锁。
  因而Thread-2和Thread-0相互等待,死锁形成。

read() :Thread-2获取了resourceA的锁!
read() :Thread-2获取了resourceB的锁!
write():Thread-2获取了resourceB的锁!
read() :Thread-0获取了resourceA的锁!

执行结果2:

  分析: 
前四行Thread-0执行过程,中间未被打断;
注意第7行,Thread-2进入方法read(),并获得了resourceA的锁;
接下来第8行,Thread-3进入方法write(),并获得了resourceB的锁;
此时死锁形成: Thread-2要获取resourceB,所以在等待Thread-3释放resourceB的锁;
Thread-3要获取resourceA,所以在等待Thread-2释放resourceA的锁;
二者互相等待,死锁形成。

read() :Thread-0获取了resourceA的锁!
read() :Thread-0获取了resourceB的锁!
write():Thread-0获取了resourceB的锁!
write():Thread-0获取了resourceA的锁!
read() :Thread-3获取了resourceA的锁!
read() :Thread-3获取了resourceB的锁!
read() :Thread-2获取了resourceA的锁!
write():Thread-3获取了resourceB的锁!

执行结果3:

未发生死锁(偶然情况),程序正常结束

read() :Thread-2获取了resourceA的锁!
read() :Thread-2获取了resourceB的锁!
write():Thread-2获取了resourceB的锁!
write():Thread-2获取了resourceA的锁!
read() :Thread-0获取了resourceA的锁!
read() :Thread-0获取了resourceB的锁!
write():Thread-0获取了resourceB的锁!
write():Thread-0获取了resourceA的锁!
read() :Thread-1获取了resourceA的锁!
read() :Thread-1获取了resourceB的锁!
write():Thread-1获取了resourceB的锁!
write():Thread-1获取了resourceA的锁!
read() :Thread-3获取了resourceA的锁!
read() :Thread-3获取了resourceB的锁!
write():Thread-3获取了resourceB的锁!
write():Thread-3获取了resourceA的锁!

三、Java与多线程相关的几个方法

方法synchronized()

1.synchronized作为方法修饰符,第1部分示例代码com.xujinliang.producer_consumer.Inventory.class中有这方面的运用。
public synchronized void methodSync()
{

//….

}
它锁定的是调用这个同步方法对象。也就是说,当特定对象obj1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的类所初始化的另一对象obj2却可以任意调用这个被加了synchronized关键字的方法。
上边的示例代码等同于如下代码:
public void methodSync()
{
	synchronized (this)    
	{
      		 //…..
	}
}

2.synchronized用来同步块,第2部分示例代码com.xujinliang.deadlock.DeadLock.class有这方面的应用。
public void method(Object obj)
{
	synchronized(obj)
    	{
       		//…..
   	 }
}
这时,锁就是obj这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。
但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
{
	private byte[] lock = new byte[0];  // 特殊的instance变量
        public void method()
        {
           synchronized(lock) { //… }
        }
        //…..
}
注:零长度的byte数组对象创建起来将比任何对象都要节省资源:
查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。


方法wait()、notify()、notifyAll()和sleep() 

Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,确切地说Obj.wait(),Obj.notify()必须在synchronized(Obj){...}语句块内,也就是wait()与notify()是针对已经获取了Obj锁的线程进行操作。
wait()使已经获取对象锁后的线程主动释放对象锁,同时使其所在线程休眠,直到有其它线程调用该对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。
notify()就是对对象锁的唤醒操作。
需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。
Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。


你可能感兴趣的:(多线程,死锁,notify,wait,notifyAll)