ArrayBlockingQueue详解

1.介绍

ArrayBlockingQueue是一个阻塞式的队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。底层以数组的形式保存数据(实际上可看作一个循环数组)。常用的操作包括 add ,offer,put,remove,poll,take,peek。

前三者add offer put 是插入的操作。后面四个方法是取出的操作。他们之间的区别和关联:

add: 内部实际上获取的offer方法,当Queue已经满了时,抛出一个异常。不会阻塞。

offer:当Queue已经满了时,返回false。不会阻塞。

put:当Queue已经满了时,会进入等待,只要不被中断,就会插入数据到队列中。会阻塞,可以响应中断。

取出方法中 remove和add相互对应。也就是说,调用remove方法时,假如对列为空,则抛出一场。另外的,poll与offer相互对应。take和put相互对应。peek方法比较特殊,前三个取出的方法,都会将元素从Queue的头部溢出,但是peek不会,实际上只是,获取队列头的元素。peek方法也不会阻塞。当队列为空时,直接返回Null。


2.对比LinkedBlockingQueue

LinkedBlockingQueue也是一个阻塞式的队列,与ArrayBlockingQueue的区别是什么呢?

LinkedBlockingQueue保存元素的是一个链表。其内部有一个Node的内部类,其中有一个成员变量 Node next。就这样形成了一个链表的结构,要获取下一个元素,只要调用next就可以了。而ArrayBlockingQueue则是一个数组。

LinkedBlockingQueue内部读写(插入获取)各有一个锁,而ArrayBlockingQueue则读写共享一个锁。


3.选择LinkedBlockingQueue还是ArrayBlockingQueue

个人感觉大多数场景适合使用LinkedBlockingQueue。在JDK源码当中有说明,LinkedBlockingQueue比ArrayBlockingQueue有更高的吞吐量,但是性能表现更难预测(也就是说相比ArrayBlockingQueue性能表现不稳定,但是也很稳定了)。

为什么会有吞吐量的区别,个人以为可能是ArrayBlockingQueue两个锁的缘故,在大量并发的情况下,插入和读取都很多时,就会造成一点的时间浪费。

还有一点,应为LinkedBlockingQueue创建时,默认会直接创建一个Integer.MAX_VALUE的数组,当插入少,读取多时,就会造成很大的空间浪费。而LinkedBlockingQueue实际上实在等需要的时候才会创建一个Node节点。

4.源码分析

下面从源码的角度来看,ArrayBlockingQueue的实现。JDK版本是1.8。


4.1 保存数据的结构

/** The queued items */
final Object[] items;
可以看到,是一个Object的数组。

4.2全局锁

/** Main lock guarding all access */
final ReentrantLock lock;
注视也说明了,这是一个掌管所有访问操作的锁。全局共享。都会使用这个锁。

4.3 add 和 offer

public boolean add(E e) {
    return super.add(e);
}
public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();  // 一直等到获取锁
    try {
        if (count == items.length)  //假如当前容纳的元素个数已经等于数组长度,那么返回false
            return false;
        else {
            enqueue(e);		// 将元素插入到队列中,返回true
            return true;
        }
    } finally {
        lock.unlock();		//释放锁
    }
}
把他们放在一起,实际上super.add(e)里面就是调用的offer方法,当offer返回false时,就抛出一个异常,否则返回true。我们直接分析offer方法。

他的实现逻辑是这样子的。

一直等待获取锁 - > 当获取到锁之后,比较当前的元素个数与数组长度,当相等时,那么队列已经满了,无法插入,返回false->否则进行入队操作,返回true。

4.4 put

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();  //可中断的获取锁
    try {
        while (count == items.length)	//当线程从等待中被唤醒时,会比较当前队列是否已经满了
            notFull.await();  //notFull = lock.newCondition 表示队列不满这种状况,假如现场在插入的时候
        enqueue(e);		//当前队列已经满了时,则需要等到这种情况的发生。
    } finally {			//可以看出这是一种阻塞式的插入方式
        lock.unlock();
    }
}

4.5 poll

如前文所说,poll方法与offer相互对应,见源码

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();	//假如当前队列中的元素为空,返回null,否则返回出列的元素
    } finally {
        lock.unlock();
    }
}

4.6 take

take方法和put方法相互对应,他一定要拿到一个元素,除非线程被中断。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)	//线程在刚进入 和 被唤醒时,会查看当前队列是否为空
            notEmpty.await();	//notEmpty=lock.newCondition表示队列不为空的这种情况。假如一个线程进行take
        return dequeue();	//操作时,队列为空,则会一直等到到这种情况发生。
    } finally {
        lock.unlock();
    }
}

4.7 peek

如前文所说,peek方法不会真正的从队列中删除元素,实际上只是取出头元素而已。

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return itemAt(takeIndex); // null when queue is empty	
                                 // 实际上 itemAt 方法就是 return (E) items[i];
//也就是说 返回 数组中的第i个元素。 finally { lock.unlock(); }} 

4.8 remove

remove方法实现在Abstract方法中,很容易看懂,里面就是走的poll方法。

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

4.9 enqueue

我们再来看看真正得到入队操作,不然光看上面的截图也不明白不是。

private void enqueue(E x) {		//因为调用enqueue的方法都已经同步过了,这里就不需要在同步了
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;		//putIndex是下一个放至元素的坐标
    if (++putIndex == items.length)	//putIndex+1, 并且比较是否与数组长度相同,是的话,则从数组开头
        putIndex = 0;			//插入元素,这就是循环数组的奥秘了
    count++;				//当前元素总量+1
    notEmpty.signal();			//给等到在数组非空的线程一个信号,唤醒他们。
}

4.10 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;		//将要取出的元素指向null 表示这个元素已经取出去了
    if (++takeIndex == items.length)	//takeIndex +1,同样的假如已经取到了数组的末尾,那么就要重新开始取
        takeIndex = 0;			//这就是循环数组
    count--;				
    if (itrs != null)		
        itrs.elementDequeued();		//这里实现就比较麻烦,下次单独出一个吧,可以看看源码
    notFull.signal();		//同样 需要给 等待数组不满这种情况的线程一个信号,唤醒他们。
    return x;
}


阅读更多
个人分类: java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭