Java集合(20)——并发集合(8)——阻塞队列(1)——ArrayBlockingQueue源码分析

目录

1.概述

2.使用案例

3.源码分析

3.1 重要属性

3.2 构造方法

3.3 私有方法入队与出队

(1)入队

(2)出队

3.4 put和take方法

(1)put

(2)take

3.5 offer和poll

(1)offer

(2)poll

3.6 peek

3.7 remainingCapacity

3.8  remove

4.总结


1.概述

  • ArrayBlockingQueue 是 BlockingQueue 接口的有界阻塞队列实现类,底层采用数组来实现。

特点

  • ArrayBlockingQueue一旦创建,容量不能改变。
  • 其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。
  • ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,
    • 所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。
    • 而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,

当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:

private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true);

2.使用案例

使用案例见上一篇博客:https://blog.csdn.net/qq_34805255/article/details/101924873

 

3.源码分析

3.1 重要属性

public class ArrayBlockingQueue {

    /**
     * ArrayBlockingQueue的底层实现为此数组
     *
     * 并且可以看到此数组被final修饰,数组一旦创建,容量不可变(数组本身的性质),并且,数组引用指向的数组对象不可变
     */
    final Object[] items;

    /**
     * 队首索引位置
     */
    int takeIndex;

    /**
     * 队尾索引位置
     */
    int putIndex;

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


    /**
     * 采用ReentrantLock来保证线程安全
     */
    final ReentrantLock lock;

    /**
     * 为了保证消费(take)数据的时候如果为空的时候,进行阻塞(等待),使用了Condition
     *
     * 当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中(notEmpty可以理解为等待队列不空的等待队列)
     */
    private final Condition notEmpty;

    /**
     * 为了保证生产(put)数据的时候如果队列满的时候,进行阻塞(等待),使用了Condition
     *
     * 当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中(notFull可以理解为等待队列不满的等待队列)。
     */
    private final Condition notFull;

}
  • 底层采用final数组items实现
  • 使用锁ReentrantLock保证线程安全
  • 使用两个Condition来实现阻塞

3.2 构造方法

    /**
     * 我们在创建一个ArrayBlockingQueue时,必须传入容量用做创建数组的大小
     * 
     * 当我们不传入是否使用公平锁时,默认是非公平的
     */
    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();
    }
  • 构造函数就是初始化了一些属性,如数组,锁,两个条件队列

3.3 私有方法入队与出队

(1)入队

    /**
     * 让元素入队
     */
    private void enqueue(E x) {

        final Object[] items = this.items;
        //插入数据到队尾
        items[putIndex] = x;
        //如果队尾刚好是数组的最后一个位置,那么,插入后队尾位置变成第一个位置
        if (++putIndex == items.length)
            putIndex = 0;
        //插入后,元素数量加1
        count++;
        //通知被阻塞的消费者线程
        notEmpty.signal();
    }
  • 通过上述的可以发现:
    • ArrayBlockingQueue的底层是使用数组实现了一个环形队列,使用takeIndex标志队列的队首位置,使用putIndex标志队列的队尾位置
    • 当队尾到达数组的最后一个位置时,插入后的下一个队尾位置又会回到开头
  • 它是如何判断队列满的呢?
    • 我们在put和offer方法中都看到,即为判断队列满的语句,只有这个条件不满足,即队列不满,才会调用私有方法enqueue

(2)出队

    /**
     * 让元素出队
     */
    private E dequeue() {

        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //先用临时变量保存队首元素
        E x = (E) items[takeIndex];
        //然后将队首引用置空
        items[takeIndex] = null;
        ////如果队首刚好是数组的最后一个位置,那么,删除元素后队首位置变成第一个位置
        if (++takeIndex == items.length)
            takeIndex = 0;
        //队首元素出队后,元素数量减1
        count--;

        if (itrs != null)
            itrs.elementDequeued();
        //通知被阻塞的生产者线程
        notFull.signal();
        return x;
    }

dequeue方法也主要做了两件事情:

  • 1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]),删除队首元素的实现是通过让原来队首引用指向null,然后让该引用原来指向的堆中的元素被GC回收
  • 2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。

3.4 put和take方法

(1)put

    /**
     * 阻塞插入:
     *      即插入时,在队列满的时候阻塞,在队列不满的时候直接插入
     */
    public void put(E e) throws InterruptedException {
        //检查传入的元素不为空
        checkNotNull(e);

        final ReentrantLock lock = this.lock;

        //加可中断锁
        lock.lockInterruptibly();
        try {
            //当队列满的时候,阻塞
            while (count == items.length)
                notFull.await();
            //当队列不满的时候,入队
            enqueue(e);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

(2)take

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //当队列空的时候,阻塞
            while (count == 0)
                notEmpty.await();
            //当队列不空的时候,直接返回队首元素
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

3.5 offer和poll

(1)offer

    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //与put不同的是加的是lock
        lock.lock();
        try {
            //队列满的时候,直接返回false,表示插入不成功
            if (count == items.length)
                return false;
            //队列不满的时候,入队插入元素,返回true,表示插入成功
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

(2)poll

    public E poll() {
        final ReentrantLock lock = this.lock;
        //使用lock保证线程安全
        lock.lock();
        try {
            //队列为空,返回null,否则出队队首元素,并返回
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

3.6 peek

    public E peek() {
        final ReentrantLock lock = this.lock;
        //通过lock保证线程安全
        lock.lock();
        try {
            //当队列为空的时候返回null,否则返回队尾元素
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }

    /**
     * 当队列为空的时候,该索引位置的值为null,所以会返回null
     */
    final E itemAt(int i) {
        return (E) items[i];
    }

3.7 remainingCapacity

    /**
     * 计算剩余容量
     */
    public int remainingCapacity() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //加锁计算剩余的容量
            return items.length - count;
        } finally {
            lock.unlock();
        }
    }

3.8  remove

    /**
     * 移除指定的元素
     */
    public boolean remove(Object o) {
        //对输入元素值做检查
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        //加锁保证线程安全
        lock.lock();
        try {
            //如果队列不为空
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                //从队首开始遍历队列
                do {
                    //如果传入的元素值和该索引位置的该元素相等,则移除该元素返回true
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    //在遍历过程中,遍历指针i到最后一个位置时,它的下一个位置迭代为0,否则++就行
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

4.总结

  • 可以发现ArrayBlockingQueue基本上所有的public方法都使用独占锁ReentrantLock的对象lock进行加锁来保证线程安全
  • 仅仅在put和take方法的时候加的是可中断独占锁
  • 使用两个Condition的等待(await)-通知(signalAll)机制来实现阻塞,使用数组实现环形队列
  • 对于 ArrayBlockingQueue,我们可以在构造的时候指定以下三个参数:

    • 队列容量,其限制了队列中最多允许的元素个数,即数组的大小
    • 指定独占锁是公平锁还是非公平锁。非公平锁的吞吐量比较高,公平锁可以保证每次都是等待最久的线程获取到锁;
    • 可以指定用一个集合来初始化,将此集合中的元素在构造方法期间就先添加到队列中。

你可能感兴趣的:(#,Java容器及源码剖析)