【并发编程】基于数组结构实现的一个有界阻塞队列ArrayBlockingQueue

ArrayBlockingQueue是什么

  • ArrayBlockingQueue是最典型的有界阻塞队列。
  • 内部使用数组存储元素!
  • 初始化时需要指定容量大小。
  • 利用 ReentrantLock 实现线程安全!

ArrayBlockingQueue的适用场景

  • 在生产者-消费者模型中使用时,如果生产速度和消费速度基本匹配的情况下,可以使用ArrayBlockingQueue。
  • 当如果生产速度远远大于消费速度,则会导致队列填满,大量生产线程被阻塞。

ArrayBlockingQueue的实现原理

  • 使用独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象,也就是只能有一个线程可以进行入队或者出队操作;
  • 意味着生产者和消费者无法并行操作,在高并发场景下会成为性能瓶颈。

ArrayBlockingQueue的特点

  • 有界队列!先进先出!存取互相排斥!
  • 使用的数据结构是静态数组:容量固定,没有扩容机制;没有元素的位置也占用空间,被 null 占位;
  • 使用ReentrantLock锁:存取是同一把锁,操作的是同一个数组对象,存取互相排斥。

ArrayBlockingQueue的入队出队操作

  • 两个指针都是从队首向队尾移动,保证队列的先进先出原则!
  • 入队阻塞对象notFull:队列count=length,放不进去元素时,阻塞在该对象上。
  • 出队阻塞对象notEmpty:队列count=0,无元素可取时,阻塞在该对象上。
  • 入队操作:从队首开始添加元素,记录putIndex(到队尾时设置为0),唤醒notEmpty。
  • 出队操作:从队首开始取出元素,记录takeIndex(到队尾时设置为0),唤醒notFull。

ArrayBlockingQueue的使用方式

// 定义同步队列
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(1000);
// 放入元素
System.out.println(blockingQueue.add(9));
blockingQueue.put(10);
// 取出元素
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());

ArrayBlockingQueue的数据结构源码分析

/** The queued items */
// 数据元素数组
final Object[] items;

/** items index for next take, poll, peek or remove */
// 下一个待取出元素索引
int takeIndex;

/** items index for next put, offer, or add */
// 下一个待添加元素索引
int putIndex;

/** Number of elements in the queue */
// 元素个数
int count;

/** Main lock guarding all access */
// 内部使用的锁
final ReentrantLock lock;

/** Condition for waiting takes */
// 消费者条件队列
private final Condition notEmpty;

/** Condition for waiting puts */
// 生产者条件队列
private final Condition notFull;

ArrayBlockingQueue的构造方法源码分析

/**
 * 一个参数的构造方法,只传入数组的最大长度。
 * 直接调用非公平模式的构造方法
 */
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

/**
 * capacity:数组的长度,队列的最大长度
 * fair:公平方式,true为公平,false为非公平
 */
public ArrayBlockingQueue(int capacity, boolean fair) {
    // 传入数组的长度小于0,直接抛出异常
    if (capacity <= 0)
        throw new IllegalArgumentException();
    // 初始化数组
    this.items = new Object[capacity];
    // 初始化锁
    lock = new ReentrantLock(fair);
    // 初始化消费者的条件队列
    notEmpty = lock.newCondition();
    // 初始化生产者的条件队列
    notFull =  lock.newCondition();
}

/**
 * capacity:数组的长度,队列的最大长度
 * fair:公平方式,true为公平,false为非公平
 * c:可以将已经存在的列表初始化到阻塞队列中。
 */
public ArrayBlockingQueue(int capacity, boolean fair,
                          Collection<? extends E> c) {
    // 调用俩个参数的构造方法
    this(capacity, fair);

    // 得到当前队列的lock锁
    final ReentrantLock lock = this.lock;
    // 加锁操作:这里加锁是防止由于指令重排序导致的可见性问题。
    lock.lock(); // Lock only for visibility, not mutual exclusion
    try {
        // 定义一个数组元素的临时角标
        int i = 0;
        try {
            // 循环每个元素,放入到ArrayBlockingQueue的数组中
            for (E e : c) {
                // 元素为NULL抛出空指针异常
                checkNotNull(e);
                // 将元素放入ArrayBlockingQueue的数组中
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            // 传入数组长度大于给定的capacity的长度,会抛出异常
            throw new IllegalArgumentException();
        }
        // 赋值元素的数量到count上
        count = i;
        // 数组中元素满了,插入的计数器从0开始
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

ArrayBlockingQueue的入队方法:put(E e) 源码分析

/**
 * 往ArrayBlockingQueue中插入元素
 */
public void put(E e) throws InterruptedException {
    // 如果元素是NULL,抛出异常
    checkNotNull(e);
    // 得到当前队列的lock锁
    final ReentrantLock lock = this.lock;
    // 尝试去获取锁
    lock.lockInterruptibly();
    try {
        // 数量满了的时候,生产者队列等待
        while (count == items.length)
            notFull.await();
        // 入队
        enqueue(e);
    } finally {
        // 唤醒消费者线程
        lock.unlock();
    }
}

/**
 * 入队操作
 */
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    // 获取到当前的元素数组
    final Object[] items = this.items;
    // 在该添加的位置放入当前的元素
    items[putIndex] = x;
    // 数组中元素满了,插入的计数器从0开始。
    // 这里进行了一次加一操作,将putIndex指向下个要插入的位置
    if (++putIndex == items.length)
        putIndex = 0;
    // 元素的数量加一
    count++;
    // 准备唤醒消费者条件队列
    notEmpty.signal();
}

/**
 * 获取锁操作:lock 优先考虑获取锁,待获取锁成功后,才响应中断。
 *          lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。
 */
public void lockInterruptibly() throws InterruptedException {
    // 直接调用AQS的acquireInterruptibly方法
    sync.acquireInterruptibly(1);
}

/**
 * 获取锁:优先考虑响应中断。
 */
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    // 如果线程被中断了,抛异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取锁。tryAcquire在AQS中,与ReentrantLock的实现方式一致
    if (!tryAcquire(arg))
        // 循环的获取锁,优先考虑中断
        doAcquireInterruptibly(arg);
}

/**
 * 循环的获取锁,优先考虑中断
 */
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    // 得到当前的节点
    final Node node = addWaiter(Node.EXCLUSIVE);
    // 定义失败标志位true
    boolean failed = true;
    try {
        for (;;) {
            // 得到当前节点的上一个(前置)节点,前置节点为null,会抛出空指针异常
            final Node p = node.predecessor();
            // 如果前置节点是头结点,并且尝试获取锁成功
            if (p == head && tryAcquire(arg)) {
                // 把当前的节点设置为头结点
                setHead(node);
                // 去掉前驱节点的指向,方便GC去回收线程
                p.next = null; // help GC
                // 变更失败标志位false
                failed = false;
                // 跳出循环
                return;
            }
            // 代码执行到这里,说明尝试获取锁,但是获取锁失败了。
            // 阻塞前的准备工作操作成功(状态是-1的时候成功)
            // 将线程阻塞,等待他去唤醒。唤醒后返回线程的中断状态!
            // 这里的代码在AQS中,与ReentrantLock的实现方式一致
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        // 上面代码抛出异常的时候,会执行这里的逻辑
        if (failed)
            // 取消获取锁的逻辑。cancelAcquire在AQS中,与ReentrantLock的实现方式一致
            cancelAcquire(node);
    }
}

/**
 * 条件队列唤醒的逻辑
 */
public final void signal() {
    // 不是当前线程,直接抛出异常!
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取条件队列的头结点
    Node first = firstWaiter;
    // 条件队列的头结点不是null,尝试去唤醒头结点
    if (first != null)
        doSignal(first);
}

/**
 * 循环去唤醒单个节点
 */
private void doSignal(Node first) {
    do {
        // 如果当前节点的下一个节点是null。说明唤醒这个就没有其他节点了。
        if ( (firstWaiter = first.nextWaiter) == null)
            // 无节点的时候设置尾结点为null
            lastWaiter = null;
        // 将当前节点的指向情况,方便GC去回收
        first.nextWaiter = null;
    // 这里的循环条件为条件队列转同步队列,transferForSignal在AQS中,与CyclicBarrier的实现方式一致
    // 转同步队列失败并且节点存在会一直循环
    // 转同步队列成功或者条件队列中没有节点,跳出循环!
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

ArrayBlockingQueue的出队方法:take() 源码分析

/**
 * ArrayBlockingQueue中减少元素
 */
public E take() throws InterruptedException {
    // 得到当前队列的lock锁
    final ReentrantLock lock = this.lock;
    // 尝试去获取锁
    lock.lockInterruptibly();
    try {
        // 队列中无数据的时候,消费者队列等待
        while (count == 0)
            // 消费者队列等待
            notEmpty.await();
        // 返回出队的结果
        return dequeue();
    } finally {
        // 唤醒生产者线程
        lock.unlock();
    }
}

/**
 * 出队操作
 */
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    // 获取到当前要出队的元素
    E x = (E) items[takeIndex];
    // 将要出队位置的元素变为null方便GC去回收
    items[takeIndex] = null;
    // 出队到最后一个,下一个出队的计数器从0开始。
    // 这里进行了一次加一操作,将takeIndex指向下个要出队的位置
    if (++takeIndex == items.length)
        takeIndex = 0;
    // 元素总数量减一
    count--;
    // 迭代器不为空的时候
    if (itrs != null)
        // 这里的逻辑主要是头结点为空的时候,清空所有迭代器!
        // 迭代器需要去重写:iterator()定义
        itrs.elementDequeued();
    notFull.signal();
    // 返回当前要出队的元素
    return x;
}

结束语

  • 获取更多本文的前置知识文章,以及新的有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你对MySQL有非常深入的了解
  • 关注公众号,每天持续高效的了解并发编程!
  • 关注公众号,后续持续高效的了解spring源码!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
posted @ 2022-02-04 21:31  程序java圈  阅读(58)  评论(0编辑  收藏  举报