java并发:阻塞队列之ArrayBlockingQueue

第一节 阻塞队列

1.1 初识阻塞队列

队列以一种先进先出(FIFO)的方式管理数据,阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是

  • 在队列为空时,获取元素的线程会等待队列变为非空;
  • 当队列满时,存储元素的线程会等待队列可用。

生产者-消费者模式:

阻塞队列常用于生产者和消费者的场景,生产者线程可以定期的把中间结果存到阻塞队列中,而消费者线程把中间结果取出并在将来修改它们。

队列会自动平衡负载,如果生产者线程集运行的比消费者线程集慢,则消费者线程集在等待结果时就会阻塞;如果生产者线程集运行的快,那么它将等待消费者线程集赶上来。

简单解说一下如何理解上表,add(e)、offer(e)、put(e)等均为阻塞队列的插入方法,但它们的处理方式不一样,add(e)方法可能会抛出异常,而put(e)方法可能一直处于阻塞状态。

下面解说一下这些处理方式:

  • 抛出异常

当阻塞队列满时,往队列里插入元素时会抛出IllegalStateException("Queue full")异常;

当阻塞队列为空时,从队列里获取元素时会抛出NoSuchElementException异常。

  • 返回特殊值

针对插入方法,该方法返回是否成功,成功则返回true;

针对移除方法,该方法从队列里拿出一个元素,如果没有则返回null。

  • 一直阻塞

当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到将数据放入队列或是响应中断退出;

当阻塞队列为空时,如果消费者线程试图从队列里take元素,队列会一直阻塞消费者线程,直到队列可用。

  • 超时退出

当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程会退出;

当阻塞队列为空时,队列会阻塞消费者线程一段时间,如果超过一定的时间,消费者线程会退出。

 

1.2 Java中的阻塞队列

java.util.concurrent包提供了几种不同形式的阻塞队列,如:

  • 数组阻塞队列 — ArrayBlockingQueue
  • 链表阻塞队列 — LinkedBlockingQueue
  • 优先级阻塞队列 — PriorityBlockingQueue
  • 延时队列 — DelayQueue

1.3 ArrayBlockingQueue

下面简单介绍一下数组阻塞队列:

ArrayBlockingQueue是一个由数组支持的有界阻塞队列,此队列按照先进先出的原则维护元素的顺序;其内部有两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue的类图如下:

其定义如下:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /*
     * Much of the implementation mechanics, especially the unusual
     * nested loops, are shared and co-maintained with ArrayDeque.
     */

    /**
     * Serialization ID. This class relies on default serialization
     * even for the items array, which is default-serialized, even if
     * it is empty. Otherwise it could not be declared final, which is
     * necessary here.
     */
    private static final long serialVersionUID = -817911632652898426L;

    /** The queued items */
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

 

其构造函数如下:

    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and default access policy.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and the specified access policy.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    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();
    }

    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity, the specified access policy and initially containing the
     * elements of the given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @param c the collection of elements to initially contain
     * @throws IllegalArgumentException if {@code capacity} is less than
     *         {@code c.size()}, or less than 1.
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            final Object[] items = this.items;
            int i = 0;
            try {
                for (E e : c)
                    items[i++] = Objects.requireNonNull(e);
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

解读:

ArrayBlockingQueue 的内部有一个数组 items,用来存放队列元素,putindex 表示入队元素下标,takelndex 表示出队元素下标,count 表示队列中元素的个数。

从定义可知,这些变量并没有使用 volatile修饰,这是因为访问这些变量的操作都是在锁块内,而加锁己经保证了锁块内变量的内存可见性了。

独占锁 lock 用来保证入队、出队操作的原子性,它保证了某个时间只能有一个线程可以进行入队、出队操作;notEmpty、 notFull 条件变量用来进行入队、出队的同步。

 

Note:

对于ArrayBlockingQueue,可以选择是否需要公平性;所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即:

  • 先阻塞的生产者线程,可以先往队列里插入元素;
  • 先阻塞的消费者线程,可以先从队列里获取元素。

数组阻塞队列的公平性是使用可重入锁实现的,需要在性能上付出代价,只有在的确非常需要的时候再使用它。

可以使用以下代码创建一个公平的阻塞队列:

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000, true);

 

添加元素

offer方法的代码如下:

    /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.  This method is generally preferable to method {@link #add},
     * which can fail to insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

解读:

如果待插入元素 e为 null,则抛出 NullPointerException异常。

如果队列中有空闲,则插入成功后返回 true;如果队列己满,则返回 false。

Note:

该方法是非阻塞的。

 

put方法的代码如下:

    /**
     * Inserts the specified element at the tail of this queue, waiting
     * for space to become available if the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

解读:

如果待插入元素 e为 null,则抛出 NullPointerException异常。

如果队列中有空闲,则插入成功后返回 true;如果队列己满,则阻塞当前线程(调用 notFull 的 await()方法把当前线程放入 notFull 的条件队列,并释放锁),直到队列有空闲后插入成功后再返回。

Note:

put方法使用 lock.locklntenuptibly()获取独占锁,所以这个方法可以被中断,即:当前线程在获取锁的过程中被其他线程设置了中断标志,则当前线程会抛出 IntenuptedException 异常而返回。

 

获取元素

poll方法的代码如下:

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

解读:

如果队列为空,则返回 null。

Note:

该方法是非阻塞的。

 

take方法的代码如下:

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

解读:

如果队列为空,则阻塞当前线程,直到队列不为空时获取元素返回。

Note:

take方法使用 lock.locklntenuptibly()获取独占锁,所以这个方法可以被中断,即:当前线程在获取锁的过程中被其他线程设置了中断标志,则当前线程会抛出 IntenuptedException 异常而返回。

 

第二节 生产者-消费者示例

一个生产者-N个消费者,程序功能:在一个目录及它的所有子目录下搜索所有文件,打印出包含指定关键字的文件列表。

package com.test;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

public class BlockingQueueTest {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("Enter base directory (e.g. /usr/local/jdk1.6.0/src): ");
        String directory = in.nextLine();
        System.out.print("Enter keyword (e.g. volatile): ");
        String keyword = in.nextLine();
        final int FILE_QUEUE_SIZE = 10;
        final int SEARCH_THREADS = 100;
        BlockingQueue<File> queue = new ArrayBlockingQueue<File>(FILE_QUEUE_SIZE);
        FileEnumerationTask enumerator = new FileEnumerationTask(queue, new File(directory));
        new Thread(enumerator).start();
        for (int i = 1; i <= SEARCH_THREADS; i++)
            new Thread(new SearchTask(queue, keyword)).start();
    }
}

/**
 * This task enumerates all files in a directory and its subdirectories.
 */
class FileEnumerationTask implements Runnable {
    /**
     * Constructs a FileEnumerationTask.
     * 
     * @param queue
     *            the blocking queue to which the enumerated files are added
     * @param startingDirectory
     *            the directory in which to start the enumeration
     */
    public FileEnumerationTask(BlockingQueue<File> queue, File startingDirectory) {
        this.queue = queue;
        this.startingDirectory = startingDirectory;
    }

    public void run() {
        try {
            enumerate(startingDirectory);
            queue.put(DUMMY); // DUMMY是最后一个被放入队列的元素,当消费者访问到该元素时,说明所有元素都已经被消费
        } catch (InterruptedException e) {
        }
    }

    /**
     * Recursively enumerates all files in a given directory and its
     * subdirectories
     * 
     * @param directory
     *            the directory in which to start
     */
    public void enumerate(File directory) throws InterruptedException {
        File[] files = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory())
                enumerate(file);
            else
                queue.put(file);
        }
    }

    public static File DUMMY = new File("");
    private BlockingQueue<File> queue;
    private File startingDirectory;
}

/**
 * This task searches files for a given keyword.
 */
class SearchTask implements Runnable {
    /**
     * Constructs a SearchTask.
     * 
     * @param queue
     *            the queue from which to take files
     * @param keyword
     *            the keyword to look for
     */
    public SearchTask(BlockingQueue<File> queue, String keyword) {
        this.queue = queue;
        this.keyword = keyword;
    }

    public void run() {
        try {
            boolean done = false;
            while (!done) {
                File file = queue.take();
                if (file == FileEnumerationTask.DUMMY) {
                    queue.put(file);
                    done = true;
                } else
                    search(file);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
        }
    }

    /**
     * Searches a file for a given keyword and prints all matching lines.
     * 
     * @param file
     *            the file to search
     */
    public void search(File file) throws IOException {
        Scanner in = new Scanner(new FileInputStream(file));
        int lineNumber = 0;
        while (in.hasNextLine()) {
            lineNumber++;
            String line = in.nextLine().trim();
            if (line.contains(keyword))
                System.out.printf("%s:%d    %s%n", file.getPath(), lineNumber, line);
        }
        in.close();
    }

    private BlockingQueue<File> queue;
    private String keyword;
}

解读:

上述程序展示了如何使用阻塞队列来控制线程集,生产者线程枚举在所有子目录下的所有文件并把它们放到一个阻塞队列中,与此同时还启动了大量的搜索线程,每个搜索线程从队列中取出一个文件,打开它,打印出包含关键字的所有行,然后取出下一个文件。

上述代码使用了一个小技巧来在工作结束后终止线程:生产者线程把一个虚拟对象放入队列,当搜索线程取到这个虚拟对象时,就将其放回并终止线程(这类似于在行李输送带上放一个写着“最后一个包”的虚拟包);这里放回的目的是让其他搜索线程拿到该对象以终止线程。

 

第三节 对比分析

ArrayBlockingQueue在生产者放入数据和消费者获取数据时共用同一个锁对象,当前线程获取锁后,其他入队和出队操作的线程都会在被阻塞挂起后被放入 lock 锁的 AQS 阻塞队列,这意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue。

按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者的并行运行;Doug Lea之所以没有这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

ArrayBlockingQueue还有一个明显的不同于LinkedBlockingQueue的地方,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象,这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。

小结:

 

posted @ 2016-04-15 12:56  时空穿越者  阅读(6818)  评论(0编辑  收藏  举报