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

来源:互联网 发布:排施工计划软件 编辑:程序博客网 时间:2024/06/18 18:30

前面讲过线程之间的同步问题;同步问题主要是为了保证对共享资源的并发访问不会出错,主要的思想是一次只让一个线程去访问共享资源,我们是通过加锁的方法实现。但是有时候我们还需要安排几个线程的执行次序,而在系统内部线程的调度是透明的,没有办法准确的控制线程的切换。所以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 0
原创粉丝点击