Java并发编程:阻塞队列ArrayBlockingQueue

目录

一、为什么需要阻塞队列

二、阻塞队列接口BlockingQueue

三、ArrayBlockingQueue的实现原理

1、成员变量

2、构造器

3、几个重要的方法

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


注:jdk1.7

一、为什么需要阻塞队列

我们之前都学过队列,如ArrayList,LinkedList等,这些队列都是不涉及到线程管理的,其实它们属于非阻塞队列

使用非阻塞队列的时候有一个很大问题就是:它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略(wait,notify,notifyAll),这样实现起来就非常麻烦。

举个例子:一个线程从一个空的非阻塞队列中取元素,此时因为队列里没有元素会报IndexOutOfBoundsException;当向一个满了的非阻塞队列中添加元素也会带来异常。所以我们需要在增或删时不断地去判断这个非阻塞队列是满的或是空的,也就是说增和删需要是两个线程,不满足条件时就wait(),满足条件了就notifyAll()各个处于阻塞中的线程。可以参数我之前写的消费者-生产者模型。

但其实在java.util.concurrent包下提供主要的几种阻塞队列,它们会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。

概括来说,阻塞队列的优势就体现在阻塞添加和阻塞删除

  • 阻塞添加:当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。
  • 阻塞删除:队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)

二、阻塞队列接口BlockingQueue

以上的阻塞队列接口BlockingQueue继承自Queue接口,因此先来看看阻塞队列接口提供的主要方法

public interface BlockingQueue<E> extends Queue<E> {

    //将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
    //在成功时返回 true,如果此队列已满,则抛IllegalStateException。 
    boolean add(E e); 

    //将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量) 
    // 将指定的元素插入此队列的尾部,如果该队列已满, 
    //则在到达指定的等待时间之前等待可用的空间,该方法可中断 
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 

    //将指定的元素插入此队列的尾部,如果该队列已满,则一直等到(阻塞)。 
    void put(E e) throws InterruptedException; 

    //获取并移除此队列的头部,如果没有元素则等待(阻塞), 
    //直到有元素将唤醒等待线程执行该操作 
    E take() throws InterruptedException; 

    //获取并移除此队列的头部,在指定的等待时间前一直等到获取元素, //超过时间方法将结束
    E poll(long timeout, TimeUnit unit) throws InterruptedException; 

    //从此队列中移除指定元素的单个实例(如果存在)。 
    boolean remove(Object o); 
}  

除此之外,还有继承于Queue接口的方法

    //获取但不移除此队列的头元素,没有则跑异常NoSuchElementException 
    E element(); 

    //获取但不移除此队列的头;如果此队列为空,则返回 null。 
    E peek(); 

    //获取并移除此队列的头,如果此队列为空,则返回 null。 
    E poll();

上述操作进行分类

插入方法 add(E e) 添加成功返回true,失败抛IllegalStateException异常
offer(E e) 成功返回 true,如果此队列已满,则返回 false
put(E e) 将元素插入此队列的尾部,如果该队列已满,则一直阻塞
删除方法 remove(Object o) 移除指定元素,成功返回true,失败返回false
poll() 获取并移除此队列的头元素,若队列为空,则返回 null
take() 获取并移除此队列头元素,若没有元素则一直阻塞
检查方法 element() 获取但不移除此队列的头元素,没有元素则抛异常
peek() 获取但不移除此队列的头;若队列为空,则返回 null

注意:黑色字的方法表示非阻塞队列也具有。对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。非阻塞队列中的方法都没有进行同步措施。

通常情况下我们都是通过这3类红色字方法操作阻塞队列,本篇我们主要来看看ArrayBlockingQueue的实现原理和简单使用~

 

三、ArrayBlockingQueue的实现原理

1、成员变量

先看一下ArrayBlockingQueue类中的几个成员变量

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

    //存储数据的数组 
    final Object[] items;

    // 获取数据的索引,主要用于take,poll,peek,remove方法
    int takeIndex;

    // 添加数据的索引,主要用于 put, offer, or add 方法
    int putIndex;

    // 队列元素的个数 
    int count;

    // 控制并非访问的锁 
    final ReentrantLock lock;

    //notEmpty条件对象,用于通知take方法队列已有元素,可执行获取操作
    private final Condition notEmpty;

    //notFull条件对象,用于通知put方法队列未满,可执行添加操作
    private final Condition notFull;

    //迭代器
    transient Itrs itrs = null;

}

可见,ArrayBlockingQueue中用来存储元素的实际上是一个数组:

takeIndex和putIndex分别表示队首元素和队尾元素的下标;

count表示队列中元素的个数;

lock是一个可重入锁;

notEmpty和notFull是等待条件。

对于notEmpty条件对象则是用于存放等待或唤醒调用take方法的线程,告诉他们队列已有元素,可以执行获取操作。同理notFull条件对象是用于等待或唤醒调用put方法的线程,告诉它们,队列未满,可以执行添加元素的操作。takeIndex代表的是下一个方法(take,poll,peek,remove)被调用时获取数组元素的索引,putIndex则代表下一个方法被调用时元素添加到数组中的索引。

2、构造器

ArrayBlockingQueue的构造器有三个重载版本

public ArrayBlockingQueue(int capacity) {

}

public ArrayBlockingQueue(int capacity, boolean fair) {
 
}

public ArrayBlockingQueue(int capacity, boolean fair,Collection<? extends E> c) {

}

第一个构造器只有一个参数用来指定容量;

第二个构造器可以指定容量和公平性;

第三个构造器可以指定容量、公平性以及用另外一个集合进行初始化。

3、几个重要的方法

1)阻塞添加

我们先看一下ArrayBlockingQueue的add方法调用了父类AbstractQueue的add方法:

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

add()中调用了offer()方法:

public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                insert(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
}

可见offer()方法先获取了锁,并且获取的是可中断锁,然后判断当前元素个数是否等于数组的长度,如果相等,则返回false;若不等于即队列非满,则加入insert(e)。

接下来看一下insert方法:

private void insert(E x) {
    //通过putIndex索引对数组进行赋值
    items[putIndex] = x;
    //索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
    putIndex = inc(putIndex);
    ++count;  //队列中元素数量加1
    //唤醒调用take()方法的线程,执行元素获取操作。
    notEmpty.signal();
}

final int inc(int i) {
    return (++i == items.length) ? 0 : i;
}

这是一个private方法,插入成功后,通过notEmpty唤醒正在等待取元素的线程

这其实很好理解,当putIndex索引大小等于数组长度时,需要将putIndex重新设置为0,这是因为当前队列执行元素获取时总是从队列头部获取,而添加元素从中从队列尾部获取所以当队列索引(从0开始)与数组长度相等时,下次我们就需要从数组头部开始添加了。

因为其底层是一个数组,不要思维定式于队首一定是数组中的第一个元素!

最后来看一下阻塞添加方法put()

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

乍一看与offer方法有点类似,不过是队列元素已满时,当前线程将会被notFull条件对象挂起(调用await方法)加到等待队列中,直到队列有空档才会唤醒执行添加操作,offer方法是直接返回false!最后也是通过insert方法添加元素。

其实队列满时,put线程加入条件对象notNull的等待队列中

有移除线程执行移除操作,移除成功同时唤醒put线程

2)阻塞移除

如果懂了阻塞添加的几个方法,那么理解起阻塞移除来就很简单了~

同样,我们先看poll方法

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

同样获取了锁,如果队列内元素为空返回null

如果非空,那就可以进行移除extract()

private E extract() {
        final Object[] items = this.items;
        //获取要删除的对象
        E x = this.<E>cast(items[takeIndex]);
        items[takeIndex] = null;
        //takeIndex索引加1并判断是否与数组长度相等,如果相等说明已到尽头,恢复为0
        takeIndex = inc(takeIndex);
        --count;  //队列个数减1
        //删除了元素说明队列有空位,唤醒notFull条件对象添加线程,执行添加操作
        notFull.signal();
        return x;
}

final int inc(int i) {
        return (++i == items.length) ? 0 : i;
}

然后是remove方法

 public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            for (int i = takeIndex, k = count; k > 0; i = inc(i), k--) {
                if (o.equals(items[i])) {
                    removeAt(i);
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
}

首先线程先获取锁,再一步判断队列count>0,这点是保证并发情况下删除操作安全执行。接着获取下一个要添加源的索引putIndex以及takeIndex索引 ,作为后续循环的结束判断,因为只要putIndex与takeIndex不相等就说明队列没有结束。然后通过while循环找到要删除的元素索引,执行removeAt(i)方法删除,在removeAt(i)方法中实际上做了两件事,一是首先判断队列头部元素是否为删除元素,如果是直接删除,并唤醒添加线程,二是如果要删除的元素并不是队列头元素,那么执行循环操作,从要删除元素的索引removeIndex之后的元素都往前移动一个位置,那么要删除的元素就被removeIndex之后的元素替换,从而也就完成了删除操作。
 

    void removeAt(int i) {
        final Object[] items = this.items;
        // if removing front item, just advance
        if (i == takeIndex) {
            items[takeIndex] = null;
            takeIndex = inc(takeIndex);
        } else {
            // slide over all others up through putIndex.
            for (;;) {
                int nexti = inc(i);
                if (nexti != putIndex) {
                    items[i] = items[nexti];
                    i = nexti;
                } else {
                    items[i] = null;
                    putIndex = i;
                    break;
                }
            }
        }
        --count;
        notFull.signal();
}

可见,类似offer方法不过是拆成了两个部分,因为需要不断地判断队头是否可以删除。

而使用 take()方法就十分简单了

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

有就删除没有就阻塞,注意这个阻塞是可以中断的,如果队列没有数据那么就加入notEmpty条件队列等待(有数据就直接取走,方法结束),如果有新的put线程添加了数据,那么put操作将会唤醒take线程,执行take操作。也就不需要remove中的循环判断了~

懂的自然懂

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

import java.util.concurrent.ArrayBlockingQueue;

public class Abqtest {
    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize);
     
    public static void main(String[] args)  {
        Abqtest abqtest = new Abqtest();
        Producer producer = abqtest.new Producer();
        Consumer consumer = abqtest.new Consumer();
         
        producer.start();
        consumer.start();
    }
     
    class Consumer extends Thread{
        public void run() {
            consume();
        }
        private void consume() {
            while(true){
                try {
                    queue.take();
                    System.out.println("取走一个元素,剩余"+queue.size()+"个元素");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     
    class Producer extends Thread{
        public void run() {
            produce();
        }
        private void produce() {
            while(true){
                try {
                    queue.put(1);
                    System.out.println("插入一个元素,剩余空间:"+(queueSize-queue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

可见阻塞队列代码要简单得多,消费者里是take,生产者里是put,不需要再单独考虑同步和线程间通信的问题,一切交给ArrayBlockingQueue来实现:)


参考资料:

1、https://blog.csdn.net/javazejian/article/details/77410889

2、https://www.cnblogs.com/dolphin0520/p/3932906.html

3、http://blog.csdn.net/zzp_403184692/article/details/8021615

猜你喜欢

转载自blog.csdn.net/qq_39192827/article/details/86265892