二十五、JAVA多线程(四、生产者和消费者问题)

一、生产者和消费者问题分析

线程通信:不同的线程执行不同的任务,如果这些任务有某种关系,线程之间必须能够通信,协调完成工作。

经典的生产者和消费者案例(Producer/Consumer):
     分析案例:
              1):生产者和消费者应该操作共享的资源(实现方式来做)。
              2):使用一个或多个线程来表示生产者(Producer。

              3):使用一个或多个线程来表示消费者(Consumer)。

生产者消费者的示意图:


在这里体现了面向对象的设计理念:低耦合.

       高(紧)耦合: 直接使用生产者把肉包子给消费者,那么生产者中得存在消费者的引用,同理,消费者要消费生产者生产的肉包子,消费者中也得存在生产者对象的引用. 例子: 主板和集成显卡。

//高(紧)耦合:
//生产者
public class  Producer{
     private  Consumer  con;//消费者对象
}
//消费者
public  class    Consumer{
        private  Producer  pro;//消费者对象
}

       低(松)耦合:使用一个中间对象,屏蔽了生产者和消费者直接的数据交互.  例子:主板和独立显卡。

//低(松)耦合:
//共享资源
public  class ShareResource{
}
//生产者
public class  Producer{
       private  ShareResource  resource;//共享资源对象
}
//消费者
public  class    Consumer{
         private  ShareResource  resource;//共享资源对象
}


二、实现生产者和消费者案例

    我们模拟罐头的生产和消费,罐头有苹果罐头和橘子罐头,罐头还应该有生产日期批次。

1.创建共享资源类Can

代码演示:

package consumer_producer;

//共享资源对象
public class Can {
	private String type;
	private String date;
	
	/**
	 * 生产者向共享资源中存储数据
	 * @param type	存储的罐头类型
	 * @param date	存储罐头的生产日期批次
	 */
	public void push(String type,String date){
		this.type = type;
		this.date = type;
	}
	
	/**
	 * 消费者从共享资源中取出数据并打印
	 */
	public void popup(){
		System.out.println(this.type+"--->"+this.date);
	}
}

2.创建生产者类Producer

代码演示:

package consumer_producer;

//生产者
public class Producer implements Runnable{
	//共享资源对象
	private Can can = null;
	
	public Producer(Can can){
		this.can = can;
	}


	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			if(i%2==0){
				can.push("apple", "2018-6-1-001");
			}else{
				can.push("orange", "2018-6-1-002");
			}
		}
	}
}

3.创建消费者类Consumer

代码演示:

package consumer_producer;

//消费者
public class Consumer implements Runnable{
	//共享资源对象
	private Can can = null;
	
	public Consumer(Can can){
		this.can = can;
	}
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
				can.popup();
		}
	}
}
4.创建测试类Test

代码演示:

package consumer_producer;

public class Test {
	public static void main(String[] args) {
		Can can = new Can();
		
		new Thread(new Producer(can)).start();
		new Thread(new Consumer(can)).start();
	}
}

代码结果:



我们发现现在代码暂时没什么大问题,我们加入Thread.sleep(100),让问题明显

修改Can类

代码演示:

package consumer_producer;

//共享资源对象
public class Can {
    private String type;
    private String date;
    
    /**
     * 生产者向共享资源中存储数据
     * @param type    存储的罐头类型
     * @param date    存储罐头的生产日期批次
     */
    public void push(String type,String date){
        this.type = type;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.date = date;
    }
    
    /**
     * 消费者从共享资源中取出数据并打印
     */
    public void popup(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.type+"--->"+this.date);
    }
}

再次运行代码结果:


代码分析:我们能够看到生产者出现了生产产品和生产批次出现了不匹配,以及消费者出现了重复消费的情况,并且苹果和橘子不再交替出现。


三、解决生产者和消费者案例问题

问题分析:出现上图原因:①生产者先生产了橘子,消费者还没有消费,生产者又生产出了苹果,导致消费出现重复消费苹果的现象


②生产者,生产完苹果,然后生产橘子,这时候还没来得及修改生产批次,出现消费者就开始消费了


③生产者,生产完橘子,然后生产苹果,这时候还没来得及修改生产批次,出现消费者就开始消费了



问题1:出现生产批次缭乱的情况。
解决方案:只要保证在生产产品和日期批次的过程保持同步,中间不能被消费者线程进来取走数据,可以使用同步代码块/同步方法/Lock机制来保持同步性。
问题2:应该出现生产一个数据,消费一个数据,结果应该交替出现。
解决方案: 得使用等待和唤醒机制.

解决问题1:

修改Can类

代码演示:

package consumer_producer;

//共享资源对象
public class Can {
	private String type;
	private String date;
	
	/**
	 * 生产者向共享资源中存储数据
	 * @param type	存储的罐头类型
	 * @param date	存储罐头的生产日期批次
	 */
	synchronized public void push(String type,String date){
		this.type = type;
		this.date = date;
	}
	
	/**
	 * 消费者从共享资源中取出数据并打印
	 */
	synchronized public void popup(){
		System.out.println(this.type+"--->"+this.date);
	}
}

代码分析:生产的步骤加上synchronized保证生产步骤同步,就能够解决生产产品和日期批次缭乱问题。

解决问题2:

同步锁池:
      同步锁必须选择多个线程共同的资源对象
      当前生产者在生产数据的时候(先拥有同步锁),其他线程就在锁池中等待获取锁。
      当线程执行完同步代码块的时候,就会释放同步锁,其他线程开始抢锁的使用权。
多个线程只有使用相同的一个对象的时候,多线程之间才有互斥效果,我们把这个用来做互斥的对象称之为,同步监听对象/同步锁。
    同步锁对象可以选择任意类型的对象即可,只需要保证多个线程使用的是相同锁对象即可。
    因为,只有同步监听锁对象才能调用wait和notify方法,所以,wait和notify方法应该存在于Object类中,而不是Thread类中

线程通信-wait和notify方法介绍:

java.lang.Object类提供类两类用于操作线程通信的方法:
wait():执行该方法的线程对象释放同步锁,JVM把该线程存放到等待池中,等待其他的线程唤醒该线程。
notify:执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待。
notifyAll():执行该方法的线程唤醒在等待池中等待的所有的线程,把线程转到锁池中等待。
*注意:上述方法只能被同步监听锁对象来调用,否则报错IllegalMonitorStateException..

假设A线程和B线程共同操作一个X对象(同步锁),A,B线程可以通过X对象的wait和notify方法来进行通信,流程如下:
    1:当A线程执行X对象的同步方法时,A线程持有X对象的锁,B线程没有执行机会,B线程在X对象的锁池中等待。
    2:A线程在同步方法中执行X.wait()方法时,A线程释放X对象的锁,进入A线程进入X对象的等待池中。
    3:在X对象的锁池中等待锁的B线程获取X对象的锁,执行X的另一个同步方法.
    4:B线程在同步方法中执行X.notify()方法时,JVM把A线程从X对象的等待池中移动到X对象的锁池中,等待获取锁。
    5:B线程执行完同步方法,释放锁.A线程获得锁,继续执行同步方法。


修改Can类

代码演示:

package consumer_producer;

//共享资源对象
public class Can {
	private String type;
	private String date;
	// 表示检查当前共享资源状态是否为空
	private boolean isEmpty = true;

	/**
	 * 生产者向共享资源中存储数据
	 * 
	 * @param type
	 *            存储的罐头类型
	 * @param date
	 *            存储罐头的生产日期批次
	 */
	synchronized public void push(String type, String date) {
		try {
			while (!isEmpty) {//当前共享资源状态不空,等待消费者来消费
				//使用同步锁对象调用,表示当前线程释放同步锁,进入等待池(休眠)
				//只能等待被其他线程唤醒
				this.wait();
			}
			//-----生产开始-----
			this.type = type;
			Thread.sleep(10);
			this.date = date;
			//-----生产结束-----
			//修改共享资源状态不为空
			isEmpty=false;
			//唤醒一个消费者
			this.notify();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

	/**
	 * 消费者从共享资源中取出数据并打印
	 */
	synchronized public void popup() {
		try {
			while(isEmpty){
				//当共享资源状态为空,释放同步锁,进入等待吃(休眠)
				//等待生产者生产,然后被唤醒
				wait();
			}
			Thread.sleep(10);
			//-----消费开始-----
			System.out.println(this.type + "--->" + this.date);
			//-----消费结束-----
			//修改共享资源状态为空
			isEmpty=true;
			//唤醒一个生产者生产
			this.notify();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
}

代码运行结果:

完成交替生产

我们来最后一次修改,完成多个生成者消费者

修改Can类

代码演示:

package consumer_producer;

//共享资源对象
public class Can {
	private String type;
	private String date;
	// 表示检查当前共享资源状态是否为空
	private boolean isEmpty = true;

	/**
	 * 生产者向共享资源中存储数据
	 * 
	 * @param type
	 *            存储的罐头类型
	 * @param date
	 *            存储罐头的生产日期批次
	 */
	synchronized public void push(String type, String date) {
		try {
			while (!isEmpty) {//当前共享资源状态不空,等待消费者来消费
				//使用同步锁对象调用,表示当前线程释放同步锁,进入等待池(休眠)
				//只能等待被其他线程唤醒
				this.wait();
			}
			//-----生产开始-----
			this.type = type;
			Thread.sleep(10);
			this.date = date;
			//-----生产结束-----
			//修改共享资源状态不为空
			isEmpty=false;
			//唤醒一个消费者
			this.notifyAll();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

	/**
	 * 消费者从共享资源中取出数据并打印
	 */
	synchronized public void popup() {
		try {
			while(isEmpty){
				//当共享资源状态为空,释放同步锁,进入等待吃(休眠)
				//等待生产者生产,然后被唤醒
				wait();
			}
			Thread.sleep(10);
			//-----消费开始-----
			System.out.println(this.type + "--->" + this.date);
			//-----消费结束-----
			//修改共享资源状态为空
			isEmpty=true;
			//唤醒一个生产者生产
			this.notifyAll();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
}

修改Test类

代码演示:

package consumer_producer;

public class Test {
	public static void main(String[] args) {
		Can can = new Can();
		
		new Thread(new Producer(can)).start();
		new Thread(new Producer(can)).start();
		new Thread(new Consumer(can)).start();
		new Thread(new Consumer(can)).start();
	}
}

代码运行结果:



四、线程通信-使用Lock和Condition接口

    wait和notify方法,只能被同步监听锁对象来调用,否则报错IllegalMonitorStateException。那么现在问题来了,Lock机制根本就没有同步锁了,也就没有自动获取锁和自动释放锁的概念。因为没有同步锁,所以Lock机制不能调用wait和notify方法。解决方案:Java5中提供了Lock机制的同时提供了处理Lock机制的通信控制的Condition接口。
从Java5开始,可以:
      1):使用Lock机制取代synchronized 代码块和synchronized 方法。
      2):使用Condition接口对象的 await,signal,signalAll方法取代Object类中的 wait,notify,notifyAll方法。

我们只用修改Can类即可

代码演示:

package consumer_producer_lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//共享资源对象
public class Can {
	private String type;
	private String date;
	// 表示检查当前共享资源状态是否为空
	private boolean isEmpty = true;
	private final Lock lock = new ReentrantLock();
	Condition condition = lock.newCondition();
	public void push(String type, String date) {
		//获取锁对象
		lock.lock();
		try {
			while (!isEmpty) {//当前共享资源状态不空,等待消费者来消费
				condition.await();
			}
			//-----生产开始-----
			this.type = type;
			Thread.sleep(10);
			this.date = date;
			//-----生产结束-----
			//修改共享资源状态不为空
			isEmpty=false;
			//唤醒一个消费者
			condition.signalAll();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();//释放锁对象
		}

	}
	public void popup() {
		//获取锁对象
		lock.lock();
		try {
			while(isEmpty){
				//当共享资源状态为空,释放同步锁,进入等待吃(休眠)
				//等待生产者生产,然后被唤醒
				condition.await();
			}
			Thread.sleep(10);
			//-----消费开始-----
			System.out.println(this.type + "--->" + this.date);
			//-----消费结束-----
			//修改共享资源状态为空
			isEmpty=true;
			//唤醒一个生产者生产
			condition.signalAll();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}



五、死锁

    多线程通信的时候很容易造成死锁,死锁无法解决,只能避免:当A线程等待由B线程持有的锁,而B线程正在等待A线程持有的锁时,发生死锁现象,JVM不检测也不试图避免这种情况,所以程序员必须保证不导致死锁。
     避免死锁法则: 当多个线程都要访问共享的资源A,B,C时,保证每一个线程都按照相同的顺序去访问他们,比如都先访问A,接着B,最后C。

哲学家吃面条的故事


Thread类中过时的方法:
suspend():使正在运行的线程放弃CPU,暂停运行。
resume():是暂停的线程恢复运行。
-----------------------------------------------------------------------------------------------------------------
*注意:因为容易导致死锁,所以已经被废弃了、
死锁情况:
     A线程获得对象锁,正在执行一个同步方法,如果B线程调用A线程的suspend方法,此时A线程暂停运行,此时A线程放弃CPU,但是不会放弃占用的锁。


  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值