Java之生产者-消费者模式

一,生产者-消费者模式

所谓的生产者-消费者模式是指生产者不断生成产品,消费者不断消耗产品。但是产品数量有一定的限制,当生成者生产的产品超过了限额就必须停止生产,直到数量小于限额才可以继续生产;当产品数量为零时,消费者不能消费,直到有产品才可以消费。

二,synchronized关键字、wait()方法、notify()方法

1,synchronized关键字
synchronized是java中的一个关键字,是一种同步锁,当有多个线程试图访问同一个代码块时,为了避免冲突,使用synchronized修饰该代码块锁定当前的对象,这样在同一时刻只能有一个线程操作,其他线程阻塞,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
synchronized可以修饰的的对象有以下几种:
<1>修饰一个代码块,被修饰的代码块称为同步语句块,作用范围是用{}括起的代码,作用的对象是调用该代码块的对象。
<2>修饰一个方法,被修饰的方法称为同步方法,作用范围是整个方法,作用的对象是调用该方法的对象。
<3>修饰一个静态方法,作用范围是整个静态方法,作用对象是这个类的所有对象。
<4>修饰一个类,作用范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象。
2,wait()方法和notify()方法
和sleep()方法是属于Thread类的方法不同,wait()方法和sleep()方法是属于Object类的方法,由于Object类是所有类的根类,因此所有类都有这两个方法。
<1>当调用wait()方法时,会释放当前得到的对象的锁。既然要释放锁,那么必须要先获得锁,因此wait()方法必须要在同步方法或者同步代码块中执行,即synchronized修饰的方法或者代码块中。
<2>当调用notify()方法时,会随机唤醒等待获取该对象锁的一个线程,使用notifyAll()方法可以唤醒所有等待获取该对象锁的线程。与wait()方法相似,要唤醒其他等待获取对象锁的线程,就必须自己先拥有该对象锁,所以sleep()也必须在synchronized代码块中执行。

三,代码实例

我们的基本思路是使用1个线程来生产小球,8个线程来消费小球,当小球数量超过5个时生产者不再生产,当没有小球时消费者不再消费,生产和消费是同时进行的,利用线程争取球对象,不是生产一个消费一个。
1,先创建四个类:main类用来创建对象、启动线程,Ball用来实现生产方法和消费方法,Productor类用来调用生产方法,Consumer类用来调用消费方法。

package com.yzd0129.ProCon;

import java.util.ArrayList;
import java.util.List;

public class main {
    
    
	public static void main(String[] args) {
    
    
		
	}

}

package com.yzd0129.ProCon;

import java.util.List;

//生产和消费的方法
public class Ball {
    
    


}

package com.yzd0129.ProCon;

import java.util.List;
//生产者
public class Productor implements Runnable{
    
    


}

package com.yzd0129.ProCon;

import java.util.List;
//消费者
public class Consumer implements Runnable{
    
    

}

2,在mian类中创建一个保存小球的数组balls,将数组balls使用构造方法传入Ball类,在Ball类定义生产和消费方法。

List<Ball> balls=new ArrayList();//球数组
package com.yzd0129.ProCon;

import java.util.List;

//生产和消费的方法
public class Ball {
    
    
	List balls;
	//构造方法   将balls数组传入
	public Ball(List balls) {
    
    
		this.balls=balls;
	}
	
	//生产方法    
	public  void put() {
    
    
	    //锁定balls对象
		synchronized(balls) {
    
    
			while(true) {
    
    
				if(balls.size()<5) {
    
    
					//如果球数量小于5个    往里添加   100ms后通知消费者取球
					Ball ball = new Ball(balls);
					balls.add(ball);
					//看添加之后生产者中有多少个小球
					System.out.println(Thread.currentThread().getName()+"put:"+balls.size());
					try {
    
    
						Thread.sleep(100);
					} catch (InterruptedException e) {
    
    
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					//唤起消费者线程
					balls.notify();

				}
				else {
    
    
					
					//等待
					try {
    
    
						//超过5个  没有容量 生产者线程等待
						System.out.println(Thread.currentThread().getName()+"wait");
						balls.wait();
					} catch (InterruptedException e) {
    
    
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}

			}
		}

	}
	
	//消费方法
	public  void take() {
    
    
	    //锁定小球对象
		synchronized(balls) {
    
    
			while(true) {
    
    
				//如果没球了  消费者则等待
				if(balls.size()==0) {
    
    
					
					try {
    
    
						//消费者线程在等待
						System.out.println(Thread.currentThread().getName()+"wait");
						balls.wait();
						//一旦调用wait方法  锁被释放   由于之前生产者一直被唤醒只是没有得到锁(下else代码)   
						//所以锁一旦被释放生产者立马抢到   不用调用notify方法通知
					} catch (InterruptedException e) {
    
    
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				else {
    
    
					//球数组     不为空     删除队列中最后一个元素
					balls.remove(balls.size()-1);
					//看拿走了哪个元素
					System.out.println(Thread.currentThread().getName()+"take:"+balls.size());
					try {
    
    
						Thread.sleep(100);
					} catch (InterruptedException e) {
    
    
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					//唤醒生产者   容器不满  生产则可以继续往里添加
					balls.notify();
				}
			}
		}

	}

}

<1>在put()和take()方法中,由于都是对balls对象操作,因此要使用synchronized关键字,使得每次对balls的操作只有一个。
<2>在put()方法中,如果小球数组长度小于5(即小球数量不足5个)则表示可以继续往里添球,于是创建Ball对象ball,将其添加进balls中,并且通知消费者(balls.notify())可以消费小球了。由于生产者对象已经在执行,所以唤醒的只能是消费者对象了。如果小球数组长度超过5,那么生产者对象进入等待状态(balls.wait()),等待消费者消费了小球使小球数量小于5之后,再被唤醒。
<3>在take()方法中,也同样使用synchronized关键字。当balls数组长度为0(即没球了),消费者对象进入等待状态(balls.wait()),此时不用唤醒消费者,因为在之前put方法中,小球长度小于5时,生产者在生产,不处于wait状态。如果balls数组长度不为0(表示有球可以消费),则消费者将最后一个小球移除,同时唤醒生产者(balls.notify),通知生产者ball是数组长度小于5,可以生产。
3,在main类中创建一个球对象b,生产者对象pro,一个消费者对象con。在Productor类中调用put()方法,在Consumer类中调用take()方法.

        Ball b = new Ball(balls);
		Productor pro = new Productor(b);//生产者对象
		Consumer con = new Consumer(b);//消费者对象
	private Ball b;
	//构造方法  将小球Ball类对象b传入
	public Productor(Ball b) {
    
    
		this.b=b;
	}

	@Override   
	//调用Ball类中的put方法
	public  void run() {
    
    
		while(true) {
    
    
			 b.put();
		}
		
	}
	private Ball b;
	//构造方法   将小球Ball类对象b传入
	public Consumer(Ball b) {
    
    
		this.b=b;
	}

	@Override
    //调用Ball中的take方法
	public  void run() {
    
    
		while(true) {
    
    
			 b.take();
		}
		
	}

4,在main类中创建1个线程执行生产,创建8个线程执行消费。

		//创建一个线程执行生产
		for(int i=0;i<1;i++) {
    
    
			new Thread(pro,"productor-"+i).start();
			//System.out.println("生产线程启动成功");
		}
		//创建8个线程执行消费
		for(int i=0;i<8;i++) {
    
    
			new Thread(con,"consumer-"+i).start();
			//System.out.println("消费线程"+i+"启动成功");
		}

5,结果分析
在这里插入图片描述
在这里插入图片描述
<1>当生产者生产完5个小球后,生产者进入等待状态。当消费者拿走5个小球后,消费者进入等待状态。由于生产和消费是同时进行的,当消费者进入等待状态的时候,有时是全部消费者在等待,有时还来不及全部消费者等待生产者就被唤醒生产了,所以有时消费者等待的线程个数不一样。
<2>一旦某个消费者线程拿到锁后,该轮5次消费就是同一个线程。因为此时该消费者线程正在进行,没有调用wait()方法释放锁,其他消费者线程无法抢到锁进行消费。同时使用notify()方法唤醒等待中的线程,等待中的线程有两种:一种是其他消费者线程,一种是生产者线程。在上面的解释中我们提到了其他消费者线程由于该消费者没有调用wait方法释放锁,所以不可能得到锁,当小球数量为0时,所有的消费者线程又都进入了等待状态,所以其他消费者线程不可能被唤醒。因此最后被唤醒的是生产者线程,通知生产者线程有容量可以生产了,当小球数量为0时,锁被释放,生产者抢到锁,进行生产。

猜你喜欢

转载自blog.csdn.net/weixin_43722843/article/details/113406144