线程通信(3) - 使用阻塞队列(Blocking Queue)控制线程通信

BlockingQueue

BlockingQueue是队列的一种,但是他并不是作为一个容器,而是作为线程同步的工具。
BlockingQueue具有一个特征,当生存者线程试图向BlockingQueue中放入元素的时候,如果该队列已经满了,则该线程被阻塞;当消费者线程视图从BlockingQueue中取出元素时,如果该队列已空,则线程被阻塞。

程序中两个线程交替向BlockingQueue中放入元素,取出元素,就能很好的控制线程的通信。


阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

这里写图片描述
抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(”Queue full”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。

返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。

一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。

超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。


阻塞队列的种类

Java中主要提供5种阻塞队列

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:同步队列,该队列的存,取必须同步交替进行。

Java在线程池中,就用到了这些阻塞队列。

1.ArrayBlockingQueue
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。

默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问队列。为了保证公平性,通常会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列。

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

访问者的公平性是使用可重入锁实现的,代码如下。

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
}

2.LinkedBlockingQueue
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

3.PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

4.DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。
缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。

5.SynchronousQueue
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。使用以下构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列。

SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。


BlockingQueue 代码示例

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

class Consumer extends Thread{
    private BlockingQueue bq;
    public Consumer(BlockingQueue 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);
        }
    }
}

public class Test{
    public static void main(String[] args){
        //创建一个容量为1的ArrayBlockingQueue
        BlockingQueue bq = new ArrayBlockingQueue<>(1);
        //创建3个生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        //启动一个消费者线程
        new Consumer(bq).start();
    }
}

ArrayBlockingQueue的集合容量为1,因此3个生产者线程无法连续加入元素,必须等消费者取出一个元素之后,3个生产者才能加入一个元素


阻塞队列实现原理

如果队列是空的,消费者会一直等待,当生产者添加元素时,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,如何让生产者和消费者进行高效率的通信呢?让我们先来看看JDK是如何实现的。

使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。通过查看JDK源码发现ArrayBlockingQueue使用了Condition来实现,代码如下。

private final Condition notFull;
private final Condition notEmpty;
public ArrayBlockingQueue(int capacity, boolean fair) {
    // 省略其他代码
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
}

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        insert(e);
    } finally {
            lock.unlock();
    }
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return extract();
    } finally {
        lock.unlock();
    }
}

private void insert(E x) {
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal();
}

你可能感兴趣的:(Java,Concurrency)