Thinking in Java---线程通信+三种方式实现生产者消费者问题

前面讲过线程之间的同步问题;同步问题主要是为了保证对共享资源的并发访问不会出错,主要的思想是一次只让一个线程去访问共享资源,我们是通过加锁的方法实现。但是有时候我们还需要安排几个线程的执行次序,而在系统内部线程的调度是透明的,没有办法准确的控制线程的切换。所以Java提供了一种机制来保证线程之间的协调运行,这也就是我们所说的线程调度。在下面我们会介绍三种用于线程通信的方式,并且每种方式都会使用生产者消费者问题进行检验。

一。使用Object类提供的线程通信机制
Object类提供了wait(),notify(),notifyAll()三个方法进行线程通信。这三个方法都必须要由同步监视器对象来调用,具体由分为以下两种情况:
1)对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
2)对于使用synchronized修饰的同步代码块,同步监视器是synchronized后面括号里的对象,所以必须使用该对象调用这三个方法。
也就是说,这三个方法只能用于synchronized做同步的线程通信。对着三个方法的具体解释如下:
wait():导致当前线程等待,直到其它线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程。该wait()方法还可以传入一个时间参数,这时候等到指定时间后就会自动苏醒。调用wait()方法的当前线程会释放对该同步监视器的锁定。
notify():唤醒在此同步监视器上等待的单个线程。如果当前有多个线程在等待,则随机选择一个。注意只有当前线程放弃对该同步监视器的锁定以后(使用了wait()方法),才可以执行被唤醒的线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。同样只要在当前线程放弃对同步监视器的锁定之后,才可以执行被唤醒的线程。

使用这种通信机制模拟的生产者消费者问题如下:

package lkl1;

///生产者消费者中对应的缓冲区
//生产者可以向缓冲区中加入数据,消费者可以消耗掉缓冲区中的数据
//注意到缓冲区是限定了大小的,所以使用循环队列的思想进行模拟

public class Buffer {
//根据循环队列的思想,如果out==in,则表示当前缓冲区为空,不可以进行消费
    //如果(in+1)%n==out,则表示当前缓冲区为满,不可以进行生产(这样会浪费一个空间)

    private int n; ///缓冲区大小
    private int num; //当前元素个数

    //定义一个大小为n的缓冲区
    private int buffer[];
    //表示当前可以放置数据的位置,初始为0
    private int in=0;
    //表示当前可以读取数据的位置,初始为0
    private int out=0;

    Buffer(int n){
        this.n=n;
        buffer=new int[n];
        num=0;
    }
    //下面是生产和消费的方法

    //生产操作,向缓冲区中加入一个元素x
    public synchronized void product(int x){

        try{
            if((in+1)%n==out){
                wait(); //如果缓冲区已满,则阻塞当前线程
            }
            else{
                buffer[in]=x;
                in=(in+1)%n;
                System.out.println(Thread.currentThread().getName()+"生产一个元素: "+x);
                num++;
                System.out.println("当前元素个数为: "+num);
                notifyAll(); //唤醒等待当前同步资源监视器的线程
            }
        }
        catch(InterruptedException ex){
            ex.printStackTrace();
        }
    }

    ///消费操作,一次取出一个元素
    public synchronized void comsumer(){

        try{
            if(in==out){ //如果缓冲区为空,阻塞当前线程
                wait();
            }
            else{
                int xx=buffer[out];
                out=(out+1)%n;
                num--;
                System.out.println(Thread.currentThread().getName()+"消费了一个元素: "+xx);
                System.out.println("当前元素个数为: "+num);
                notifyAll();
            }
        }
        catch(InterruptedException ex){
            ex.printStackTrace();
        }
    }
}
package lkl1;

import java.util.Random;

//生产者线程
//会不断的往缓冲区中加入元素
public class Product extends Thread{

    //当前线程操作的缓冲区对象
    private Buffer buffer;
    private Random rand;
    Product(){}
    Product(Buffer buffer){
        this.buffer=buffer;
        rand=new Random();
    }
    public void run(){
        while(true){
            //向缓冲区中添加一个随机数
            buffer.product(rand.nextInt(100));
        }
    }
}
package lkl1;

//生产者线程
public class Consumer extends Thread{

    private Buffer buffer;
    Consumer(){}
    Consumer(Buffer buffer){
        this.buffer=buffer;
    }
    public void run(){  
        while(true){ //每次都消耗掉缓冲区中的一个元素
            buffer.comsumer();
        }
    }
}
package lkl1;
//测试
public class BufferTest {

    public static void main(String[] args){
        Buffer buffer = new Buffer(10); 

        //一个生产者,多个消费者
        new Product(buffer).start(); 
        new Consumer(buffer).start();
        new Consumer(buffer).start();
    }
}

二。使用Condition控制线程通信
前面我们讲同步方式的时候,除了synchronized关键字,还讲了可以使用Lock进行显示的加锁。在使用Lock对象时,是不存在隐式的同步监视器的,所以也就不能使用上面的线程通信方式了。其实在使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition类可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象。
Condition提供了三个方法:await(),signal(),signallAll();这三个方法和Object对象的三个方法的基本用法是一样的。其实我们可以这样认为,Lock对象对应了我们上面讲的同步方法或同步代码块,而Condition对象对应了我们上面讲的同步监视器。还要注意的是,Condition实例被绑定在一个Lock对象上,要获得指定Lock的Condition实例,需要调用Lock对象的newCondtion()方法即可。
下面使用Lock和Condition的组合来实现生产者消费者问题。可以看到代码基本和上面是一样的。

package lkl1;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

///生产者消费者中的缓冲区
//由一个数组代表,生产者可以向缓冲区中加入元素,消费者可以从缓冲区中取走元素
//如果缓冲区满,则生产者不能向缓冲区加入元素;如果缓冲区空,则消费者不能消费元素
//下面的程序中in表示生产者可以加入数据的位置,out表示消费者可以消费数据的位置
//in和out都会初始化为0,我们定义in==out表示缓冲区为空;(in+1)%n==out
//表示缓冲区满,但是这种判满的方式是要浪费一个空间的。

//上个例子中使用了synchronized关键字保证对缓冲区的操作的同步。
//现在需要采用Lock和Condition类进行同步的控制.
public class Buffer1 {

    private final Lock lock=new ReentrantLock();
    private final Condition con=lock.newCondition();
    private int n;
    private int buffer1[];
    private int in;
    private int out;
    private int cnt; ///记录当前缓冲区中元素个数
    Buffer1(){}
    Buffer1(int n){
        this.n=n;
        buffer1=new int[n];
        in=out=cnt=0;
    }

   //生产方法,加入元素x
   public void product(int x){
       lock.lock(); //加锁
       try{
           if((in+1)%n==out){ //如果缓冲区满,则阻塞当前线程
               con.await();
               //con.signalAll();
           }
           else{
               buffer1[in]=x;
               in=(in+1)%n;
               cnt++;
               System.out.println(Thread.currentThread().getName()+"向缓冲区中加入元素:"+x);
               System.out.println("当前缓冲区中的元素个数为: "+cnt);
               con.signalAll(); //唤醒其它线程
           }
       }
       catch(InterruptedException ex){
           ex.printStackTrace();
       }
       finally{ ///使用finally语句保证锁能正确释放
           lock.unlock();
       }
   }

   //消费方法,取走缓冲区中的一个元素
   public int consumer(){
       int x=0;
       lock.lock();
       try{
           if(in==out){ //如果缓冲区空,则阻塞当前线程
               con.await();
           }
           else{
               x=buffer1[out];
               System.out.println(Thread.currentThread().getName()+"消费元素: "+x);
               out=(out+1)%n;
               cnt--;
               System.out.println("当前元素个数为: "+cnt);
               con.signalAll(); //唤醒其它线程
           }
       }
       catch(InterruptedException ex){
           ex.printStackTrace();
       }
       finally{
           lock.unlock();
       }
       return x;
   }
}
package lkl1;

import java.util.Random;

//消费者线程
public class Consumer1 extends Thread{

    private Random rand=new Random();
    private Buffer1 buffer1; //对应的缓冲区
    Consumer1(Buffer1 buffer1){
        this.buffer1=buffer1;
    }
    public void run(){
        while(true){
            buffer1.consumer();
            try{    ///在消费者线程中加一个sleep语句,可以更好的体现线程之间的切换
                sleep(50);
            }
            catch(Exception x){
                x.printStackTrace();
            }
        }
    }
}
package lkl1;

import java.util.Random;

//生产者线程
public class Product1 extends Thread{

    private Random rand = new Random();
    private Buffer1 buffer1;
    Product1(Buffer1 buffer1){
        this.buffer1=buffer1;
    }
    public void run(){
        while(true){
            int x;
            x=rand.nextInt(100);
            buffer1.product(x);
        }
    }
}
package lkl1;

///Buffer1测试
//启动一个生产者线程,两个消费者线程
public class Buffer1Test {

    public static void main(String[] args) throws Exception{
        Buffer1 buffer1 = new Buffer1(10);
        new Product1(buffer1).start(); 
        new Consumer1(buffer1).start();
        new Consumer1(buffer1).start();
    }
}

三。使用阻塞队列(BlockingQueue)来控制线程通信
Java5提供了一个BlockingQueue接口,这个接口也是属于队列的子接口,但是他主要的作用还是用来进行线程通信,而不是当成队列用。BlockingQueue的特征是:当生产者线程试图向BlockingQueue中加入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程会被阻塞。
这样程序的两个线程通过交替的向BlocingQueue中放入元素,取出元素,即可很好的控制线程的通信。当然也不是所有的BlockingQueue的方法都支持阻塞操作的。
BlockingQueue提供了以下两个支持阻塞的方法:
put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
take():尝试从BlockingQueue的头部取出元素,如果该队列的元素为空,则阻塞该线程。
其它的Queue对应的方法,也都是支持的,但是在上面的情况下操作,不会阻塞,而是会返回false或抛出异常。
常用的BlockingQueue的实现类有以下几种:
ArrayBlockingQueue:基于数组实现的BlockingQueue队列
LinkedBlockingQueue:基于链表实现的BlockingQueue队列
PriorityBlockingQueue:优先队列对应的阻塞队列
SynchronizedQueue:同步队列,对该队列的存取必须交替进行

因为阻塞队列本身就支持生产者消费者模式,所以用阻塞队列来实现生产者消费者问题就很简单了。

package lkl;

import java.util.concurrent.BlockingQueue;

///消费者线程
public class Consumer extends Thread{

    private BlockingQueue<String>bq;
    public Consumer(BlockingQueue<String> bq){
        this.bq=bq;
    }
    public void run(){
        while(true){
            System.out.println(getName()+"消费者准备消费集合元素!");
            try{
                Thread.sleep(200);
                //尝试取出元素,如果队列以空,则线程阻塞
                bq.take();
            }
            catch(Exception ex){
                ex.printStackTrace();
            }
            System.out.println(getName()+"消费完成: "+bq);
        }
    }
}
package lkl;

import java.util.concurrent.BlockingQueue;

//生产者线程
public class Producer extends Thread{

    private BlockingQueue<String>bq;
    public Producer(BlockingQueue bq){
        this.bq=bq;
    }
    public void run(){
        String[] strArr = new String[]{
                "Java","Struts","Spring"
        };
        for(int i=0;i<99999999;i++){
            System.out.println(getName()+"生产者准备生产集合元素!");
            try{
                Thread.sleep(200);
                //尝试放入元素,如果队列已满,则线程会被阻塞
                bq.put(strArr[i%3]);
            }
            catch(Exception ex){
                ex.printStackTrace();
            }
            System.out.println(getName()+"生产完成: "+bq);
        }
    }
}
package lkl;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueTest2 {

    public static void main(String[] args){

        //创建一个容量为1的BlockingQueue
        BlockingQueue<String> bq=new ArrayBlockingQueue<>(1);

        //启动3个生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();

        //启动一个消费者线程
        new Consumer(bq).start();
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值