二十五、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,但是不会放弃占用的锁。