Android 多线程 阻塞队列

793 阅读7分钟

多线程编程-阻塞队列(BlockingQueue)

本文参考《Android进阶之光》

为什么使用阻塞队列?

  • 更好的理解线程池

  • 不用再关注线程的阻塞、同步、唤醒,阻塞队列帮我们完成,我们只需关注业务逻辑

阻塞队列简介

​ 阻塞队列常用于生产者消费者的场景,生产者是往队列中添加元素的线程,消费者是从队列中拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

常见的阻塞常见

阻塞队列有两个常用的阻塞场景,分别是:

  1. 当队列没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到数据放入队列。
  2. 当队列填满数据,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

支持以上两种阻塞场景的队列被称为阻塞队列。

BlockingQueue核心方法
方法/处理方式抛出异常返回特殊一直阻塞超时退出
放入数据add(E e)offer(E e)(true,flase)put(E e)offer(E e, long timeout, TimeUnit unit)
获取数据remove(Object o)poll()(null)take()poll(long timeout, TimeUnit unit)
  • 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalArgumentException异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常
  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。

Java中的阻塞队列

在Java中提供了7种阻塞队列,分别如下所示。

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。

  • LinkedBlockingQueue:由链表结构组成的有界阻塞队列。

  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

  • DelayQueue:使用优先级队列实现的无界阻塞队列。

  • SynchronousQueue:不存储元素的阻塞队列。

  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。

  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

主要学习一种就行,其余的只不过是实现方式不同,适用场景不同,了解一个的原理就差不离了。

ArrayBlockingQueue:

​ 数组实现的有界阻塞队列,并按照先进先出的原则对元素进行排序。默认情况下不保证线程公平访问队列(按照线程阻塞的先后顺序进行访问队列)。

fair参数设置为true,则是公平的阻塞队列,默认为flase;通常情况下,为了保证公平性会降低吞吐量。

构造方法:
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    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();
    }
    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 {
            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();
        }
    }

核心方法:
  /**
     * 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 throwing an
     * {@code IllegalStateException} if this queue is full.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws IllegalStateException if this queue is full
     * @throws NullPointerException if the specified element is null
     */
    public boolean add(E e) {
        return super.add(e);
    }

    /**
     * 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();
        }
    }

    /**
     * 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();
        }
    }

    /**
     * Inserts the specified element at the tail of this queue, waiting
     * up to the specified wait time for space to become available if
     * the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        Objects.requireNonNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0L)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

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

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

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0) {
                if (nanos <= 0L)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    /**
     * Removes a single instance of the specified element from this queue,
     * if it is present.  More formally, removes an element {@code e} such
     * that {@code o.equals(e)}, if this queue contains one or more such
     * elements.
     * Returns {@code true} if this queue contained the specified element
     * (or equivalently, if this queue changed as a result of the call).
     *
     * <p>Removal of interior elements in circular array based queues
     * is an intrinsically slow and disruptive operation, so should
     * be undertaken only in exceptional circumstances, ideally
     * only when the queue is known not to be accessible by other
     * threads.
     *
     * @param o element to be removed from this queue, if present
     * @return {@code true} if this queue changed as a result of the call
     */
    public boolean remove(Object o) {
        if (o == null) return false;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final Object[] items = this.items;
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length) i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

LinkedBlockingQueue:

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

其中Integer.MAX_VALUE大小如下:

public static final int MAX_VALUE = 2147483647; // 0x7fffffff

所以注意,如果构造一个一个LinkedBlockingQueue对象,而没有指定其容量大小,当生产者速度远大于消费者的时候,可能没等到阻塞发生,内存就耗尽了。

这两个是最常用的阻塞队列,其余的有兴趣自己可以去了解一下。

阻塞队列的实现原理

以ArrayBlockingQueue为例,参考源码,查看实现。

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -817911632652898426L;
    final Object[] items;
    int takeIndex;
    int putIndex;
    int count;
    final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;

ArrayBlocking维护的是一个Object的数组,takeIndex为队首下标,putIndex为队尾下标,count为队列元素个数,lock是个可重入锁,notEmpty和notFull是等待条件,下面通过核心方法分析。

  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();
        }
    }

put方法,先获取重入锁,并且获取的是可中断锁,然后判断当前队列元素个数是否等于数组的长度,如果相等,就notFull.await()等待条件。当此线程被其他线唤醒,通过enqueue(e)方法插入数据,最后解锁。然后查看下enqueue方法,如下所示:

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length) putIndex = 0;
        count++;
        notEmpty.signal();
    }

插入操作,通过notEmpty.signal()唤醒正在等待取元素的线程。再看take方法。

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

如果队列元素为空,则进入等待,当被唤醒的时候,执行dequeue(),并接受返回。下面是dequeue()方法。

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length) takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

获取元素,并且notFull.signal(),唤醒等会插入数据的线程,通知其当前队列已经不是满的了。

使用场景

除了线程池使用阻塞队列,还可以在生产者-消费者模式中使用阻塞队列:

首先使用Object.wait(),Object.notify()和非阻塞队列实现生产者-消费者模式,代码如下所示:

public class TestConsumerProduct {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
    public static void main(String[] args) {
        TestConsumerProduct testConsumerProduct = new TestConsumerProduct();
        Product product = testConsumerProduct.new Product();
        Consumer consumer = testConsumerProduct.new Consumer();
        product.start();
        consumer.start();
    }
    class Consumer extends Thread{
        @Override
        public void run() {
            while(true){
                synchronized(queue){
                    while(queue.size() == 0){
                        try{
                            System.out.println("队列空,等待数据");
                            queue.wait();
                        }catch(InterruptedException exception){
                            exception.printStackTrace();
                            queue.notify();
                        }
                    }
                    //每次移走队首元素
                    queue.poll();
                    queue.notify();
                }
            }
        }
    }
    class Product extends Thread{
        @Override
        public void run() {
            while(true){
                synchronized(queue){
                    while(queue.size() == queueSize){
                        try{
                            System.out.println("队列满,等待有余空间");
                            queue.wait();
                        }catch(InterruptedException e){
                            e.printStackTrace();
                            queue.notify();
                        }
                    
                    }
                    //每次插入数据
                    queue.offer(1);
                    queue.notify();
                }
            }
        }
    }
    
}

使用阻塞队列实现的生产者-消费者模式

public class TestForConsumerProducer {
    private int queueSize = 10;
    private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(queueSize);
    public static void main(String[] args) {
        TestForConsumerProducer testForConsumerProducer = new TestForConsumerProducer();
        Product product = testForConsumerProducer.new Product();
        Consumer consumer = testForConsumerProducer.new Consumer();
        product.start();
        consumer.start();
    }
    class Consumer extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(++i < 10){
               try{
                   queue.take();
                
               }catch(InterruptedException e){
                   e.printStackTrace();
               }
            }
        }
    }
    class Product extends Thread{
        @Override
        public void run() {
            int i =0;
            while(++i < 10){
                try{
                    queue.put(1);
               
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

阻塞队列实现无需单独考虑同步和线程间通信的问题,实现更简单。