欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-04-13 12:17:04
...

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

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

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

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

生产者消费者的示意图:

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

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

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

//高(紧)耦合:
//生产者
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();
	}
}

代码结果:

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


我们发现现在代码暂时没什么大问题,我们加入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);
    }
}

再次运行代码结果:

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

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


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

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

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

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

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

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

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


问题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();
		}
		
	}
}

代码运行结果:

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

完成交替生产

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

修改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();
	}
}

代码运行结果:

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



四、线程通信-使用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。

哲学家吃面条的故事

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

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

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