生产者消费者问题是多线程经常要碰到的问题,需要达成的目标是:生产者生产一个商品,消费者就要消费一个商品,然后生产者才能继续生产。
Demo1
class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
//资源
class Resource{
private int count = 1;
public synchronized void set(){
count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+count);
}
public synchronized void out(){
System.out.println(Thread.currentThread().getName()+"...消费者........."+count);
}
}
//生产者
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res = res;
}
public void run(){
while(true){
res.set();
}
}
}
//消费者
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res = res;
}
public void run(){
while(true){
res.out();
}
}
}
上面的代码有三个类,一个是资源,一个是生产者,另外一个是消费者,在资源类里有一个count计数器,代表第几个商品。在生产者消费者里定义一个资源类是为了接收同一个外面new出来资源类,这样同步函数用的this锁就唯一指向那个资源类。尽管这样,我们这个程序依然是不成功的,运行出来的结果是生产了N多个商品,才会去消费。所以我们必须想办法让生产者生产完后,就让他停下,等待消费者消费完之后生产者再继续生产。Java其实已经定义好了这个功能,那就是wait(),和notify()。
Demo2:单个生产者消费者
class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
//资源
class Resource{
private int count = 1;
private boolean flag = false;
public synchronized void set(){
if(flag)
try{this.wait();}catch(Exception e){}
count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+count);
flag = true;
this.notify();
}
public synchronized void out(){
if(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者........."+count);
flag = false;
this.notify();
}
}
//生产者
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res = res;
}
public void run(){
while(true){
res.set();
}
}
}
//消费者
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res = res;
}
public void run(){
while(true){
res.out();
}
}
}
这段代码多了一个标志flag以及wait()和notify()。我们先来了解一下什么是wait(),notify()。他们不是Thread类里的方法,而是object类里就有的,他们必须在synchronized里面使用,wait的作用是,当一个线程拿到锁进入synchronized之后,释放该锁,并让线程进入休眠状态,直到其他线程调用notify之后,wait的线程才会被唤醒,并继续持有锁。需要注意的是,notify()之后并不会马上就释放锁,唤醒一个线程,而是等待相应synchronized(){}语句块全部执行完毕之后才会将锁释放。
所以该段代码的执行流程是:先把flag置为false(可以理解为还没有商品),所以生产者不能等待,而必须去生产,生产完之后,flag置为true(告诉消费者有商品可以消费了),并唤醒消费者,如果此时cup的执行权还在生产者手里,那么生产者再次进入循环,而此时flag为true,生产者wait,释放锁,释放cpu的执行资格,进入休眠状态。那么cpu的执行资格给了消费者,此时flag为true,所以消费者必须去消费一个商品,然后把flag置为false(告诉生产者又可以生产了),最后又唤醒了生产者去生产。
对于单个生产者消费者,这样做是没有问题的,但是如果有两个生产者消费者,上面的程序又会出错,有可能出现生产了两个商品,而消费者只消费一个,或者只生产一个商品,却消费了两个。出现这种情况的原因是:假如t1,t2线程是生产者,t3,t4线程是消费者,t1首先进入,生产完一个把标志置为true,再循环之后wait,这时t2也进入,因为标志为true,也wait,然后t3进入,消费完一个把标志置为false,然后notify一个线程,注意此时t4还没进入,所以能被唤醒的只有生产者,目前为止一切正常。假如t1被唤醒了,生产完一个,标志置为true,然后notify一个线程,重点来了,这时能被唤醒的除了消费者还有可能是也在wait的t2,如果t2被唤醒,那么t2将继续执行,再次生产一个商品,这时就一次性生产了两个商品,出错了。
解决步骤:1.把if改成while,让线程每次被唤醒之后都再次去检测那个标志。当然光有这个不行,不然在上面的重点那里,t2被唤醒之后,再次去检查标志量,发现为true,被wait掉,此时所有线程都在等待状态,程序也无法执行下去(全部等待状态跟死锁又略微有点不同,死锁是几个线程共同争抢一个资源导致的,而这里是所有线程都休眠了)所以还应该有个步骤2
2.就是把notify改成notifyAll,notifyAll就是把所有线程都唤醒,当生产者消费者都被唤醒后,就不会出现1中的状况了。
下面是完整代码 Demo3:多个生产者消费者
class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//资源
class Resource{
private int count = 1;
private boolean flag = false;
public synchronized void set(){
while(flag)
try{this.wait();}catch(Exception e){}
count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+count);
flag = true;
this.notifyAll();
}
public synchronized void out(){
while(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者........."+count);
flag = false;
this.notifyAll();
}
}
//生产者
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res = res;
}
public void run(){
while(true){
res.set();
}
}
}
//消费者
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res = res;
}
public void run(){
while(true){
res.out();
}
}
}
上面多次提到锁这个概念,但是在使用synchronized时,这个锁是隐式存在的,我们不能实际操控它,而且我们notifyAll的时候,同时唤醒了生产者和消费者,实际情况是只要唤醒一方就可以了。所以在JDK1.5之后,有了一个新的机制,那就是Lock,用来代替synchronized。先看看API里的介绍,
方法摘要 | |
---|---|
void | lock() 获取锁。 |
void | lockInterruptibly() 如果当前线程未被中断,则获取锁。 |
Condition | newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。 |
boolean | tryLock() 仅在调用时锁为空闲状态才获取该锁。 |
boolean | tryLock(long time, TimeUnit unit) 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 |
void | unlock() 释放锁。 |
方法摘要 | |
---|---|
void | await() 造成当前线程在接到信号或被中断之前一直处于等待状态。 |
boolean | await(long time, TimeUnit unit) 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 |
long | awaitNanos(long nanosTimeout) 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 |
void | awaitUninterruptibly() 造成当前线程在接到信号之前一直处于等待状态。 |
boolean | awaitUntil(Date deadline) 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。 |
void | signal() 唤醒一个等待线程。 |
void | signalAll() 唤醒所有等待线程。 |
Demo4:Lock机制实现线程同步
import java.util.concurrent.locks.*;
class LockDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Resource{
private int count = 1;
private boolean flag = false;
private ReentrantLock lock = new ReentrantLock(); //获得一个锁
private Condition condition_pro = lock.newCondition(); //生产者Condition
private Condition condition_con = lock.newCondition(); //消费者Condition
public void set()throws InterruptedException{
lock.lock(); //锁上
try{
while(flag)
condition_pro.await(); //生产者等待
count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+count);
flag = true;
condition_con.signal(); //唤醒消费者
}
finally{
lock.unlock();//释放锁的动作一定要执行。
}
}
public void out()throws InterruptedException{
lock.lock();
try{
while(!flag)
condition_con.await(); //消费者等待
System.out.println(Thread.currentThread().getName()+"...消费者........."+count);
flag = false;
condition_pro.signal(); //唤醒生产者
}
finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res = res;
}
public void run(){
while(true){
try{
res.set();
}
catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res = res;
}
public void run(){
while(true){
try{
res.out();
}
catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
我们要使用这个锁,肯定得先弄一个锁,但是这个锁是一个接口,不能产生对象,好在它有几个实现类实现了这个接口,其中一个就是ReentrantLock,我们就用它new一个锁出来。刚才说了Lock是用来代替synchronized的,而Condition用来代替synchronized()括号中的对象的,所以我们还得有Condition。需要注意的是,对象是在synchronized中使用的,与synchronized相关,Condition也必须与Lock相关才能正常使用,在Lock的方法中我们可以发现有一个newCondition()方法,它就可以返回绑定到此Lock的Condition实例。那么为什么设定两个呢,接着看下面的代码,在set函数中,lock.lock到lock.unlock这段就是要同步的代码块,其他的与Demo3中的代码类似,只不过wait的时候我们可以指定是生产者wait,signal的时候指定是消费者唤醒,这样也不需要signalAll了,让一个消费者醒来程序就可以继续运行,所以这就是我们设定两个Condition的原因,这也是Lock机制比synchronized所强大的地方。另外await函数会抛出一个异常,这可能导致我们拿到锁之后,程序异常退出,锁依然没有释放,所以这里我们用了finally,让锁无论如何都会释放,这里没有catch,因此不是用来处理await异常的,await的异常被抛出去了,在外面被catch处理。out函数与set函数类似,就不解释了。