一,生产者-消费者模式
所谓的生产者-消费者模式是指生产者不断生成产品,消费者不断消耗产品。但是产品数量有一定的限制,当生成者生产的产品超过了限额就必须停止生产,直到数量小于限额才可以继续生产;当产品数量为零时,消费者不能消费,直到有产品才可以消费。
二,synchronized关键字、wait()方法、notify()方法
1,synchronized关键字
synchronized是java中的一个关键字,是一种同步锁,当有多个线程试图访问同一个代码块时,为了避免冲突,使用synchronized修饰该代码块锁定当前的对象,这样在同一时刻只能有一个线程操作,其他线程阻塞,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
synchronized可以修饰的的对象有以下几种:
<1>修饰一个代码块,被修饰的代码块称为同步语句块,作用范围是用{}括起的代码,作用的对象是调用该代码块的对象。
<2>修饰一个方法,被修饰的方法称为同步方法,作用范围是整个方法,作用的对象是调用该方法的对象。
<3>修饰一个静态方法,作用范围是整个静态方法,作用对象是这个类的所有对象。
<4>修饰一个类,作用范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象。
2,wait()方法和notify()方法
和sleep()方法是属于Thread类的方法不同,wait()方法和notify()方法是属于Object类的方法,由于Object类是所有类的根类,因此所有类都有这两个方法。
<1>当调用wait()方法时,会释放当前得到的对象的锁。既然要释放锁,那么必须要先获得锁,因此wait()方法必须要在同步方法或者同步代码块中执行,即synchronized修饰的方法或者代码块中。
<2>当调用notify()方法时,会随机唤醒等待获取该对象锁的一个线程,使用notifyAll()方法可以唤醒所有等待获取该对象锁的线程。与wait()方法相似,要唤醒其他等待获取对象锁的线程,就必须自己先拥有该对象锁,所以notify()也必须在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时,锁被释放,生产者抢到锁,进行生产。