【JUC源码】ArrayBlockingQueue源码分析

从类注释可以得到的信息如下:

  1. 有界的阻塞数组,容量一旦创建,后续大小无法修改;
  2. 元素是有顺序的,按照先入先出进行排序,从队尾插入数据数据,从队头拿数据;
  3. 队列满时,往队列中 put 数据会被阻塞,队列空时,往队列中拿数据也会被阻塞。

1.结构

ArrayBlockingQueue 的继承关系,核心成员变量及主要构造函数:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
	implements BlockingQueue<E>, java.io.Serializable {
    
    
	
	// 保存数据的数组,ArrayBlockingQueue 实际上是通过数组实现了一个循环队列
    // 注:数组大小必须在初始化的时候手动设置,没有默认大小
    final Object[] items;

    // 下次拿数据的时候的索引位置(队首)
    int takeIndex;                   						  
    // 下次放数据的索引位置(队尾)
    int putIndex;
    // 队列中已有元素的个数(<=length)
    int count;
    /**
    * 注:takeIndex和putIndex都是动态变化的,因为数组容量(length)初始化好就始终不会再变,主要的变化为两种:
    * 1.在元素入队后putIndex++,在元素出队后takeIndex++
    * 2.在putIndex或者takeIndex == length-1(到达数组末端),且count != length(队列未满)时,它俩会被重置为0
    * /


    // 可重入的锁,作用有两个
    // 1.保证在并发时对数组操作(入队/出队)的线程安全。另外,还可以设置是否公平锁
    // 2.通过条件队列(Condition)实现线程控制
    final ReentrantLock lock;

	// 入队(put)的条件队列,其实很容易理解:入队条件是队列未满
    private final Condition notFull;
    
    // 出队(take)的条件队列。其实很容易理解:出队条件是队列非空
    private final Condition notEmpty;

    
    //----------------------------------构造函数--------------------------------
    // 构造函数一:传入初始容量
    // 注: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];

        // 创建锁
        // 1.公平,即按照阻塞的先后顺序唤醒;
        // 2.非公平(默认),未阻塞或阻塞时间少的线程有机会先获得锁
        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 {
            // i 代表插入的位置
            int i = 0;
            try {
                // 此时需要注意的是,如果 c 的大小超过了数组的大小
                // 是会抛异常的
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            // 如果插入的位置,正好是队尾了,下次需要从 0 开始插入
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }
}

2.队列的继承关系

在讲 ArrayBlockingQueue 的方法源码之前,我们先来看一下 BlockingQueue 的继承关系。下图是 ArrayBlockingQueue 的类图:
在这里插入图片描述

2.1 Queue:队列的顶层接口

public interface Queue<E> extends Collection<E> {
    
    
    // 入队
    boolean add(E e);
    boolean offer(E e);
    // 出队
    E remove();  
    E poll();
    // 队首
    E peek();
    E element(); 
}

2.2 AbstractQueue:中间层

  • 所有的Queue不是直接实现Queue,而是直接继承AbstractQueue
  • AbstractQueue实现了部分方法
    • add:对offer进行了封装,若offer为false,则抛异常
    • remove:对poll进行了封装,若poll为null,则抛异常
    • element:对peek进行了封装,若peek为null,则抛异常
  • 实现了clear,清空队列
public abstract class AbstractQueue<E>
    extends AbstractCollection<E>
    implements Queue<E> {
    
    
    
    public boolean add(E e) {
    
    
    	// offer
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
    
    public E element() {
    
    
        E x = peek();
        // peek
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }
    
     public E remove() {
    
    
        E x = poll();
        // poll
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }
    
    public boolean addAll(Collection<? extends E> c) {
    
    
        if (c == null)
            throw new NullPointerException();
        if (c == this)
            throw new IllegalArgumentException();
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }
    
    public void clear() {
    
    
    	// 将所有元素弹出
        while (poll() != null)
            ;
    }
}

2.3 BlockQueue:阻塞队列

阻塞队列的顶层接口,继承了Queue接口

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

	boolean add(E e);
	// 注:子类调用remove方法时会调用AbstractQueue#remove()

    void put(E e) throws InterruptedException;
    E take() throws InterruptedException;
    
    // 阻塞一定时间
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
    E poll(long timeout, TimeUnit unit) throws InterruptedException;  
	
	// 删除指定与安娜
	boolean remove(Object o);

	//...... 
}

在阻塞队列中,上面三组出队入队方法的对比如下:

抛异常 返回特殊值 阻塞(WAITING) 阻塞一段时间
入队(满) add offer(false) put offer 过超时时间返回 false
出队(空) remove poll (null) take poll 过超时时间返回 null
队首(空) element peek (null) 暂无 暂无
  • 这些方法保证线程安全的方法都是通过锁,因此在夺锁过程中都可能发生阻塞
  • 区别只是在碰见空(满)会让相应线程进入WAITING

3.方法解析 & api

从上一部分中,我们看到了队列的主要方法无非就三个:入队、出队、队首。所以接下来我们就从这三个方法入手,来看看 ArrayBlockingQueue 的具体实现。

3.1 入队

put():队满阻塞

public void put(E e) throws InterruptedException {
    
    
        checkNotNull(e); // 入队元素不能为空
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); // 上锁,设置为可中断
        try {
    
    
            // !!!队列如果是满的,就将当前线程加入到notFull的条件队列中,然后进入阻塞状态
            // 等待某个线程执行拿走队列元素(dequeue)或者删除元素(removeAt)后,有机会被唤醒进入同步队列
            // 注:这里的while循环有double-check的意思,即防止线程已经被调度执行了,但前一刻有另一个线程put/offer/add成功了,所以他又要进入条件队列notFull中阻塞等待。
            while (count == items.length)
                notFull.await();
            enqueue(e); // 入队时数组的相关操作
        } finally {
    
    
            lock.unlock(); // 释放锁
        }
}

这里面涉及到的是AQS相关源码,感兴趣的童鞋可以参考JUC核心:AQS源码最详细万字深析(同步队列&条件队列)

offer():队满返回false

public boolean offer(E e) {
    
    
        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(); // 释放锁
        }
}

add():队满抛异常

public boolean add(E e) {
    
    
    	// !!!直接调用offer方法,若失败,就抛异常
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
}

enqueue()

入队时数组的相关操作:

  1. 获取原数组
  2. 在 putIndex(队尾) 位置放入元素
  3. 计算下一次 putIndex(队尾) ,若已经到数组末尾就将 putIndex 置为队首
  4. 随机唤醒一个出队时阻塞在 notEmpty 条件队列的线程
private void enqueue(E x) {
    
    
        // assert lock.getHoldCount() == 1; 同一时刻只能一个线程进行操作此方法
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        // putIndex 为本次插入的位置
        items[putIndex] = x;
        // 计算下次putIndex位置,若到最后了就将index置为0
    	// 注:保证不会覆盖队首的前提条件是,count==length时在上面三个方法就已经阻塞或返回了
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        // 唤醒取出线程
        notEmpty.signal();
}

3.2 出队

take():队空阻塞

public E take() throws InterruptedException {
    
    
        final ReentrantLock lock = this.lock; 
        lock.lockInterruptibly(); // 加锁,设置为可中断
        try {
    
    
            // !!!队列如果是空的,就将当前线程加入到notEmpty条件队列中,然后进入阻塞状态
            // 等待某个线程拿向队列放入元素(enqueue)后,有机会被唤醒进入同步队列
            // 注:这里的while循环有double-check的意思,即防止线程已经被调度执行了,但前一刻有另一个线程take/poll/remove成功了,所以他又要进入条件队列notFull中阻塞等待。
            while (count == 0)
                notEmpty.await();
            
            return dequeue(); // 出队时数组的相关操作
        } finally {
    
    
            lock.unlock(); // 释放锁
        }
    }

poll():队空返回null

public E poll() {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock(); // 加锁
        try {
    
    
            // !!!队列为空,直接返回null
            return (count == 0) ? null : dequeue(); // 出队时数组的相关操作
        } finally {
    
    
            lock.unlock(); // 释放锁
        }
}

remove():队空抛异常

思路同上面的add方法,都是直接调用offer/poll,但在 ArrayBlockingQueue 中没有直接实现该方法,而是在它的父类 AbstractQueue 中:
在这里插入图片描述

dequeue()

出队时数组的相关操作:

  1. 获取原数组
  2. 将 takeIndex(队首) 位置的元素删除(置为null)
  3. 计算下一次 takeIndex(队首),若已经到达数组末尾就将 takeIndex 置为队首
  4. 随机唤醒一个入队时阻塞在 notFull 条件队列的线程
private E dequeue() {
    
    
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        // takeIndex 代表本次拿数据的位置,是上一次拿数据时计算好的
        E x = (E) items[takeIndex];
        // 帮助 gc
        items[takeIndex] = null;
        // ++ takeIndex 计算下次拿数据的位置,若idx=length,idx=0
        if (++takeIndex == items.length)
            takeIndex = 0;
        // 队列实际大小减 1
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 唤醒要放入的线程
        notFull.signal();
        return x;
}

3.3 获取队首:peek

public E peek() {
    
    
        final ReentrantLock lock = this.lock;
    	lock.lock(); // 加锁
        try {
    
    
            // 循环队列,返回takeIndex位置的元素
            return itemAt(takeIndex); 
        } finally {
    
    
            lock.unlock(); // 释放锁
        }
}

3.4 删除

remove():删除指定元素

这个方法其实就是寻找要删除元素的索引,然后调用 removeAt 方法执行删除

// 传入要删除的元素,返回删除成功或者失败
public boolean remove(Object o) {
    
    
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock(); // 加锁
        try {
    
    
        	// 判断队列中是否有元素,若为空则放回false
            if (count > 0) {
    
    
                final int putIndex = this.putIndex;
                int i = takeIndex;
                // do-while循环寻找要删除元素的索引,然后调用removeAt方法执行删除
                do {
    
    
                    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(); // 释放锁
        }
    }

removeAt():删除指定位置

删除指定idx的元素,这里分为了两种情况:

  • 情况一:removeIndex == takeIdx
    1. 将removeIndex的元素置为null,表示删除了指定元素
    2. takeIdx后移(++),表示下次直接取下一个元素
  • 情况二:removeIndex != takeIdx,这里又分为两种情况:
    1. removeIndex+1 == putIdx,直接将putIdx前移一位
    2. removeIndex+1 != putIdx,通过for循环将removeIndex与putIdx间元素前移一位,直到removeIndex+1 == putIndex时执行上面情况一
void removeAt(final int removeIndex) {
    
    
    final Object[] items = this.items;
    // 情况一: 删除位置正好等于下次要拿数据的位置
    if (removeIndex == takeIndex) {
    
    
        // 下次要拿数据的位置直接置空
        items[takeIndex] = null;
        // 要拿数据的位置往后移动一位
        if (++takeIndex == items.length)
            takeIndex = 0;
        // 当前数组的大小减一
        count--;
        if (itrs != null)
            itrs.elementDequeued();
    // 情况二:删除位置不等于下次要拿数据的位置
    } else {
    
    
        final int putIndex = this.putIndex;
        for (int i = removeIndex;;) {
    
    
            // 找到要删除元素的下一个
            int next = i + 1;
            if (next == items.length)
                next = 0;
            // 2.1 下一个元素不是 putIndex
            if (next != putIndex) {
    
    
                // 下一个元素往前移动一位
                items[i] = items[next];
                i = next;
            // 2.2 下一个元素是 putIndex
            } else {
    
    
                // 删除元素
                items[i] = null;
                // 下次放元素时,应该从本次删除的元素放
                this.putIndex = i;
                break;
            }
        }
        count--;
        if (itrs != null)
            itrs.removedAt(removeIndex);
    }
    notFull.signal();
}

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/108833582