J.U.C 阻塞队列(二) - ArrayBlockingQueue

1 概述

ArrayBlockingQueued是数组实现的线程安全的有界的阻塞队列。队列按照先进先出(FIFO)的原则对元素进行排序。

通过以下关键词分析我们来更深入理解ArrayBlockingQueue

1.1 如何理解“队列”

队列这个概念非常好理解。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的“队列”。

相对于栈只支持两个基本操作:入栈 push()和出栈 pop(),对于也只支持两个操作入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素,因此队列跟栈一样,也是一种操作受限的线性表数据结构

image
1.2 如何理解“线程安全的”

在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,线程安全的队列我们叫作并发队列.

那如何实现一个线程安全的队列呢?

方式一
最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。

方式二
利用 CAS 原子操作,可以实现非常高效的并发队列。

1.3 阻塞队列

阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

1.4 有界队列

有界队列表示队列中存储数据是有限,如果队列满后在次向队列中添加数据会失败或阻塞。

2 实现一个"队列"

我们知道了,队列跟栈一样,也是一种抽象的逻辑存储结构。它具有先进先出的特性,支持在队尾插入元素,在队头删除元素。如果要想实现一个队列可以用数组来实现,也可以用链表来实现,用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列

2.1 顺序队列
2.1.1 实现原理

比起栈的数组实现,队列的数组实现稍微有点儿复杂,复杂在哪呢?对于栈来说,我们只需要一个栈顶指针就可以了。但是队列需要两个指针:一个是 head 指针,指向队头;一个是 tail 指针,指向队尾。结合下面这幅图来理解。当 a、b、c、d 依次入队之后,队列中的 head 指针指向下标为 0 的位置,tail 指针指向下标为 4 的位置。

image

当我们调用两次出队操作之后,队列中 head 指针指向下标为 2 的位置,tail 指针仍然指向下标为 4 的位置。

image

你肯定已经发现了,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组中还有空闲空间,
也无法继续往队列中添加数据了。这个问题该如何解决呢?使用循环队列

2.3 循环队列

循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。

image

我们可以看到,图中这个队列的大小为 8,当前 head=4,tail=7。当有一个新的元素 a 入队时,我们放入下标为 7 的位置。但这个时候,我们并不把 tail 更新为 8,而是将其在环中后移一位,到下标为 0 的位置。当再有一个元素 b 入队时,我们将 b 放入下标为 0 的位置,然后 tail 加 1 更新为 1。所以,在 a,b 依次入队之后,循环队列中的元素就变成了下面的样子:

image

通过这样的方法,我们成功避免了数据搬移操作。但是循环队列最关键的是,确定好队空和队满的判定条件

在用数组实现的非循环队列中,队满的判断条件是 tail == n,队空的判断条件是 head == tail。那针对循环队列,如何判断队空和队满呢?

方式一

image

就像我图中画的队满的情况,tail=3,head=4,n=8,所以总结一下规律就是:(3+1)%8=4。多画几张队满的图,你就会发现,当队满时满足以下公式 :(tail+1)%n=head。

当队列满时,图中的 tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。

3 ArrayBlockingQueue源码解析

3.1 类结构
image
3.2 实现原理

1 ArrayBlockingQueue使用循环数组实现顺序队列,

2 ArrayBlockingQueue内部存在着一个可重入的锁,当出队和入队操作时首先要获取同一把锁来保证线程安全。数组实现的队列出队和入队是要相互排斥的,因为数组删除元素需要对数据中元素进行位移

3 ArrayBlockingQueue内部存在着一个可重入的锁,同时此锁生成二个等待队列Condition(notFull,notEmpty) -- 生产者和消费者模式

  • 在入队时需要获取锁,获取锁成功后会判断队列是否已满(数组已满)。如果队列已满会将当前线程添加到notFull等待队列中等待唤醒。如果队列没有满则入队,并释放notEmpty等待队列中一个等待的线程。

  • 在出队时需要获取锁,获取锁成功后会判断队列是否为空(数组为空)。如果队列为空会将当前线程添加到notEmpty等待队列中等待唤醒。如果队列不为空则出队,并释放notFull等待队列中一个等待的线程。

3.3 核心属性
// 内部数组
final Object[] items;

// 重入锁
final ReentrantLock lock;

// 条件,用于出队等待队列
private final Condition notEmpty;

// 条件,用于入队等待队列
private final Condition notFull;

// 出队位置
int takeIndex;

// 入队位置
int putIndex;

//队列中元素数量
int count;
3.4 构造函数
//创建一个带有给定的(固定)容量和默认访问策略的 ArrayBlockingQueue。
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}
    
//创建一个具有给定的(固定)容量和指定访问策略的 ArrayBlockingQueue。
//fair 用来表示获取锁的方式是否公平[(详见ReentrantLock)](https://note.youdao.com/)
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();
}

//创建一个具有给定的(固定)容量和指定访问策略的 ArrayBlockingQueue,它最初包含给定 collection 的元素,并以 collection 迭代器的遍历顺序添加元素。
public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection c) {
        this(capacity, fair);
        //将指定容器中元素依此添加到队列中
        final ReentrantLock lock = this.lock;
        //获取独占锁,成功返回,失败则阻塞
        lock.lock();  
        try {
            int i = 0;
            try {
                //遍历容器张的元素添加到队列中
                for (E e : c) {
                    //检查添加数据不能为null,NullPointerException
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }
3.5 入队操作

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

public boolean add(E e) {
    if (offer(e)){
        return true;
    }else{
        // 抛出异常...
    }
}

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

public boolean offer(E e) {
    //插入的元素是否为null,是抛出NullPointerException异常
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //获取独占锁,成功返回,失败则阻塞
    lock.lock();

    try {
        //如果队列已满,则返回false。
        if (count == items.length){
            return false;
        }else {
            //添加元素到队列尾部
            enqueue(e);
            return true;
        }
    } finally {
        //释放锁
        lock.unlock();
    }
}

//添加元素到队列尾部
private void enqueue(E x) {
        //获取队列中数组对象
        final Object[] items = this.items;
        //在items[putIndex]插入元素
        items[putIndex] = x;
        //putIndex向数组尾部循环移动
        if (++putIndex == items.length)
            putIndex = 0;
        //队列元素数量+1    
        count++;
        //释放notEmpty等待队列中阻塞的线程
        notEmpty.signal();
    }

将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间,在成功时返回 true。

public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        //插入的元素是否为null,是抛出NullPointerException异常
        checkNotNull(e);
        //获取等待的时间
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        //获取独占锁,成功返回,失败则阻塞,可响应中断
        lock.lockInterruptibly();
        try {
            //如果队列已满,等待时间大于0。将当前线程添加notFull等待队列,并到限时阻塞当前线程
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            //添加元素到队列尾部
            enqueue(e);
            //成功返回true
            return true;
        } finally {
            //释放锁
            lock.unlock();
        }
    }

将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间

public void put(E e) throws InterruptedException {
    //插入的元素是否为null,是抛出NullPointerException异常
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //获取锁,失败则阻塞,可响应中断。
    lock.lockInterruptibly();
    try {
        //如果队列已满。将当前线程添加notFull等待队列,并阻塞当前线程
        while (count == items.length){
            notFull.await();
        }
        //添加元素到队列
       enqueue(e);
    } finally {
        //释放锁
        lock.unlock();
    }
}
3.6 出队操作

获取并移除此队列的头,如果此队列为空,则返回 null。

public E poll() {
    final ReentrantLock lock = this.lock;
    //获取锁,失败则阻塞。
    lock.lock();
    try {
        //若队列为空,则返回 null。
        //若队列不为空,返回队列头部元素
        return (count == 0) ? null : dequeue();
    } finally {
        //释放锁
        lock.unlock();
    }
}

//返回队列头部元素
private E dequeue() {
        //获取数组对象
        final Object[] items = this.items;
        //获取items[takeIndex]元素
        E x = (E) items[takeIndex];
        //将数组items[takeIndex]元素设置为null
        items[takeIndex] = null;
        //takeIndex向后循环移动
        if (++takeIndex == items.length)
            takeIndex = 0;
        //队列数量--    
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        //释放等待notFull等待队列中阻塞的线程    
        notFull.signal();
        //返回
        return x;
    }

获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        //获取等待的时间
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        //获取锁,失败则阻塞,可响应中断。
        lock.lockInterruptibly();
        try {
            //如果队列为空,等待时间大于0,将当前线程添加notEmpty等待队列,并限时阻塞当前线程
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            //返回队列头部元素
            return dequeue();
        } finally {
            //释放锁
            lock.unlock();
        }
    }

获取并移除此队列的头部,在元素变得可用之前一直等待

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    //获取锁,失败则阻塞,可响应中断。
    lock.lockInterruptibly();
    try {
        //如果队列为空,将当前线程添加notEmpty等待队列张,并阻塞当前线程
        while (count == 0){
            notEmpty.await();
        }
        //返回队列头部元素
        return dequeue();
    } finally {
        //释放锁
        lock.unlock();
    }
}


指定元素出队

  • 从此队列中移除指定元素的单个实例
    • 遍历队列数组中元素所有元素找到指定元素在数组的下标
    • 如果删除元素刚好位置和takeIndex重合,直接删除,takeIndex移动


      image
    • 删除元素b刚好位置在takeIndex和putIndex之间,直接删除,并将数组向前平移。


      image
public boolean remove(Object o) {
    //删除元素为null,直接返回失败
    if (o == null){
        return false;
    }
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock;
    //获取锁,失败则阻塞
    lock.lock();
    try {
        // 遍历内部数组的所有元素,从 takeIndex 开始,找到删除元素的下标位置
        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();
    }
}

//删除指定下标元素
void removeAt(final int removeIndex) {
        final Object[] items = this.items;
        // 判断是否在删除元素的下标是否和takeIndex重合
        if (removeIndex == takeIndex) {
            //清理takeIndex下标中的元素
            items[takeIndex] = null;
            //移动takeIndex
            if (++takeIndex == items.length)
                takeIndex = 0;
            count--;
            
            if (itrs != null)
                itrs.elementDequeued();
        } else {
            // 当和takeIndex不重合,删除的下标在takeIndex~putIndex之间。
            // 需要将 [(i+1) - putIndex ] 位置的元素,前移一位,通过覆盖 i 位置的元素,来达到删除的效果。
            final int putIndex = this.putIndex;
            for (int i = removeIndex;;) {
                int next = i + 1;
                if (next == items.length)
                    next = 0;
                if (next != putIndex) {
                    items[i] = items[next];
                    i = next;
                } else {
                    items[i] = null;
                    this.putIndex = i;
                    break;
                }
            }
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex);
        }
        //释放等待notFull等待队列中阻塞的线程  
        notFull.signal();
    }

4 ArrayBlockingQueue使用

4.1 ArrayBlockingQueue API
// 创建一个带有给定的(固定)容量和默认访问策略的 ArrayBlockingQueue。
ArrayBlockingQueue(int capacity)
// 创建一个具有给定的(固定)容量和指定访问策略的 ArrayBlockingQueue。
ArrayBlockingQueue(int capacity, boolean fair)
// 创建一个具有给定的(固定)容量和指定访问策略的 ArrayBlockingQueue,它最初包含给定 collection 的元素,并以 collection 迭代器的遍历顺序添加元素。
ArrayBlockingQueue(int capacity, boolean fair, Collection c)

// 将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则抛出 IllegalStateException。
boolean add(E e)
// 自动移除此队列中的所有元素。
void clear()
// 如果此队列包含指定的元素,则返回 true。
boolean contains(Object o)
// 移除此队列中所有可用的元素,并将它们添加到给定 collection 中。
int drainTo(Collection c)
// 最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定 collection 中。
int drainTo(Collection c, int maxElements)
// 返回在此队列中的元素上按适当顺序进行迭代的迭代器。
Iterator iterator()
// 将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则返回 false。
boolean offer(E e)
// 将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。
boolean offer(E e, long timeout, TimeUnit unit)
// 获取但不移除此队列的头;如果此队列为空,则返回 null。
E peek()
// 获取并移除此队列的头,如果此队列为空,则返回 null。
E poll()
// 获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。
E poll(long timeout, TimeUnit unit)
// 将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间。
void put(E e)
// 返回在无阻塞的理想情况下(不存在内存或资源约束)此队列能接受的其他元素数量。
int remainingCapacity()
// 从此队列中移除指定元素的单个实例(如果存在)。
boolean remove(Object o)
// 返回此队列中元素的数量。
int size()
// 获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。
E take()
// 返回一个按适当顺序包含此队列中所有元素的数组。
Object[] toArray()
// 返回一个按适当顺序包含此队列中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。
 T[] toArray(T[] a)
// 返回此 collection 的字符串表示形式。
String toString()
4.2 案例
import java.util.*;
import java.util.concurrent.*;

/*
 *   ArrayBlockingQueue是“线程安全”的队列,而LinkedList是非线程安全的。
 *
 *   下面是“多个线程同时操作并且遍历queue”的示例
 *   (01) 当queue是ArrayBlockingQueue对象时,程序能正常运行。
 *   (02) 当queue是LinkedList对象时,程序会产生ConcurrentModificationException异常。
 *
 */
public class ArrayBlockingQueueDemo1{

    // TODO: queue是LinkedList对象时,程序会出错。
    //private static Queue queue = new LinkedList();
    private static Queue queue = new ArrayBlockingQueue(20);
    public static void main(String[] args) {
    
        // 同时启动两个线程对queue进行操作!
        new MyThread("ta").start();
        new MyThread("tb").start();
    }

    private static void printAll() {
        String value;
        Iterator iter = queue.iterator();
        while(iter.hasNext()) {
            value = (String)iter.next();
            System.out.print(value+", ");
        }
        System.out.println();
    }

    private static class MyThread extends Thread {
        MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
                int i = 0;
            while (i++ < 6) {
                // “线程名” + "-" + "序号"
                String val = Thread.currentThread().getName()+i;
                queue.add(val);
                // 通过“Iterator”遍历queue。
                printAll();
            }
        }
    }
}

你可能感兴趣的:(J.U.C 阻塞队列(二) - ArrayBlockingQueue)