阻塞队列(ArrayBlockingQueue) 迭代器源码分析

本文深入分析了 ArrayBlockingQueue 的迭代器(Itrs 和 Itr)实现,探讨了其作为弱一致性迭代器如何处理并发修改,以及在不同场景下迭代器状态的维护,包括清除过期迭代器、元素删除时的处理等。


在看到 ArrayBlockingQueue 迭代器时感觉比之前看到的迭代器实现都要复杂,所有就专门提出来说一说。

ArrayBlockingQueue 迭代器与 ConcurrentHashMap 迭代器类似都是一种“弱一致性”迭代器,在遍历数组时修改数组并不会抛出 ConcurrentModificationException 异常。

为什么 ArrayBlockingQueue 迭代器复杂呢?

ArrayBlockingQueue 是由数组实现的有界队列,难就难在它是有界队列并且还要保证元素的顺序。使用数组实现有界队列,就只能使用记录队头、队尾索引的方式来实现一个循环队列。

在 ArrayBlockingQueue 中使用takeIndex记录队头索引、putIndex记录队尾索引用来操作队列。

/** items index for next take, poll, peek or remove */
int takeIndex;

/** items index for next put, offer, or add */
int putIndex;

迭代器中需要使用到takeIndex,因为从队头向队尾遍历才能最大程度的保证遍历的一致性(可以遍历到创建迭代器之后添加到队列中的元素)。

提出几个 ArrayBlockingQueue 迭代器的问题用于下面代码分析时进行思考
  • 怎样记录迭代器的遍历状态
  • 如何判断迭代器当前状态是否有效
  • 在迭代器创建完成之后并未立即使用,在多次入、出队操作或删除元素之后,迭代器会做如何处理
  • 如果迭代器没有失效会怎么处理
  • 如果迭代器失效了会怎么处理

Itrs

在 ArrayBlockingQueue 中使用 Itrs 维护这一个 Itr 链表,用于在一个队列下的多个 Itr 迭代器中共享队列元素,保证多个迭代器中的元素数据的一致性。

虽然 Itrs 这个设计增加了维护上的复杂性,但是为了保证迭代器在删除元素时,各个迭代器中能够保持一致,这个 Itrs 的设计时有必要的。Itrs 通过

  1. 跟踪 takeIndex 循环到 0 的次数。提供 takeIndexWrapped 方法,当 takeIndex 循环到 0 时,清除过期迭代。
  2. 提供 removedAt,通知所有的迭代器执行 removedAt 来保证所有的 Itr 迭代器数据保持一致。

以上的两项操作应当能够保证 Itr 迭代器间的一致性,但是增加了许多其他的操作来维护这些 Itr 迭代器。Itrs 通过一个链表和弱引用来维护 Itr 迭代器,并通过一下三种方式清空 Itr 迭代器:

  1. 当创建 Itr 迭代器时,检查链表中的 Itr 迭代器是否过期。
  2. 当 takeIndex 循环到 0 时,检查超过一次循环,但是从未被使用的迭代器。
  3. 如果队列被清空,那么所有的 Itr 迭代器都会被通知数据作废。

所以为了保证正确性,removedAt、shutdown 和 takeIndexWrapped 方法都做检查 Itr 迭代器是否过期的操作。如果元素都过期,GC 。如果决定迭代器作废或者迭代器通知自己过期,那么这些过期的元素会被清除。这个操作不需要做额外的其他操作就可完成。

Itr

当调用 iterator() 方法时,创建迭代器对象 Itr

public Iterator<E> iterator() {
   
   
   return new Itr();
}
1.重要变量

这些变量记录着迭代器的遍历状态非常重要,在每次调用next()都会去修正这些变量以维护迭代器的遍历状态。

/** Index to look for new nextItem; NONE at end */
private int cursor;

/** Element to be returned by next call to next(); null if none */
private E nextItem;

/** Index of nextItem; NONE if none, REMOVED if removed elsewhere */
private int nextIndex;

nextItem:调用next()方法的返回值
nextIndexnextItem对象的索引
cursornextIndex的下一个位置的索引
在不断调用next()方法获取元素的过程中,这三个变量以两个在前一个在后向后移动着。

/** Last element returned; null if none or not detached. */
private E lastItem;

/** Index of lastItem, NONE if none, REMOVED if removed elsewhere */
private int lastRet;

lastItem:上一次返回的元素
lastRetlastItem的索引
需要注意:lastRetnextIndexcursor 这三个变量是判断迭代器是否失效的主要依据(满足 cursor < 0 && nextIndex < 0 && lastRet < 0 时代表迭代器失效

/** Previous value of takeIndex, or DETACHED when detached */
private int prevTakeIndex;

/** Previous value of iters.cycles */
private int prevCycles;

prevTakeIndex :代表本次遍历开始的索引,每此 next 都会进行修正
prevCycles :Itrs 管理 Itr 链,它里面有个变量 cycles 记录 takeIndex 回到 0 位置的次数,迭代器的 prevCycles 存储该值
需要注意:这两个变量存储的值可能过时(在创建迭代器之后未立即使用,而对数组进行了修改),迭代器多处操作前都会通过这两个值来判断数据是否过时,以做相应的处理。

/** Special index value indicating "not available" or "undefined" */
private static final int NONE = -1;

/**
 * Special index value indicating "removed elsewhere", that is,
 * removed by some operation other than a call to this.remove().
 */
private static final int REMOVED = -2;

/** Special value for prevTakeIndex indicating "detached mode" */
private static final int DETACHED = -3;

DETACHED:专门用于prevTakeIndexisDetached方法通过其来判断迭代器状态是否失效
NONE:用于三个下标变量:cursornextIndexlastRet;这三个下标变量用于迭代功能的实现。表明该位置数据不可用或未定义
REMOVED:用于lastRetnextIndex。表明数据过时或被删除

2. 构造方法

在构造方法中对迭代器中的变量进行初始化

Itr() {
   
   
    // assert lock.getHoldCount() == 0;
    lastRet = NONE;
    final ReentrantLock lock = ArrayBlockingQueue.this.lock;
    // 在创建迭代器过程加锁,防止队列改变导致初始化错误
    lock.lock();
    try {
   
   
    	// 如果队列为空
        if (count == 0) {
   
   
            // assert itrs == null;
            // 迭代器为DETACHED模式 - 失效状态
            cursor = NONE;
            nextIndex = NONE;
            prevTakeIndex = DETACHED;
        } else {
   
   
            final int takeIndex = ArrayBlockingQueue.this.takeIndex;
            // 初始化遍历起始索引prevTakeIndex为队头索引takeIndex
            prevTakeIndex = takeIndex;
            // 下一个遍历元素nextItem为队头元素itemAt(takeIndex)
            // nextIndex为nextItem的索引
            nextItem = itemAt(nextIndex = takeIndex);	
            // 获取nextIndex下一个元素的索引
            cursor = incCursor(takeIndex);
            // 如果itrs为null,就初始化;
            if (itrs == null) {
   
   
                itrs = new Itrs(this);
            } 
			// 否则将当前迭代器注册到itrs中,并清理失效迭代器
			else {
   
   
                itrs.register(this); // in this order
                itrs.doSomeSweeping(false);
            }
            // 当前迭代器记录最新的轮数
            prevCycles = itrs.cycles;
            // assert takeIndex >= 0;
            // assert prevTakeIndex == takeIndex;
            // assert nextIndex >= 0;
            // assert nextItem != null;
        }
    } finally {
   
   
        lock.unlock();
    }
}

下面用一张存在元素的队列的示意图来展示一下初始过程各变量的状态:
在这里插入图片描述
关于 cursor 的增加操作:

private int incCursor(int index) {
   
   
    // assert lock.getHoldCount() == 1;
    if (++index == items.length)
        index = 0;
    // index == putIndex:标志着迭代结束    
    if (index == putIndex)
        index = NONE;
    return index;
}

当遍历到 putIndex ,代表数据遍历结束,应该终止迭代,将 cursor 置为 NONE 标识,cursor 置为 NONE 后会引起迭代器的终止,逻辑在 next 与 hasNext 方法中。

3. doSomeSweeping - 清除迭代器链

doSomeSweeping方法不是在调用之后并非一次将整个链表探测一遍,而是根据传入参数boolean tryHarder来选择探测范围是4或16,若在范围内未探测到无效迭代器则结束,若是探测到则扩大探测范围,将范围恢复为16,继续往下探测。

Itrs 中的三个相关变量

//记录上次探测的结束位置节点,下次从此开始往后探测。
private Node sweeper = null;
// 探测范围
private static final int SHORT_SWEEP_PROBES = 4;
private static final int LONG_SWEEP_PROBES = 16;

doSomeSweeping将清除迭代器链中无效的迭代器结点:

  1. 结点持有的Itr对象为空,说明被GC回收,说明使用线程完成迭代
  2. 迭代器ItrDETACHED 模式,对于迭代结束或数据过时的迭代器会被置于 DETACHED 模式
void doSomeSweeping(boolean tryHarder) {
   
   
    // assert lock.getHoldCount() == 1;
    // assert head != null;
	// tryHarder 为true则 probes 为 16,否则为 4
	// probes 代表本次的探测长度
    int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
    // o 代表 p 的前一个节点,用于链表中节点的删除
    Node o, p;
    // 它代表了一次探测中到达的最后一个节点
    final Node sweeper = this.sweeper;
    /**
     * passedGo:限制一次遍历
     * 	若从头开始遍历,passedGo设为true,
     * 		如果当前结点为null且probes>0可以直接break
     * 	若从中开始遍历,passedGo设为false,
     * 		如果当前结点为null且probes>0就会转回到头部继续遍历
     */
    boolean passedGo;
    if (sweeper == null
### ArrayBlockingQueue 的内部实现机制 ArrayBlockingQueue 是基于数组实现的有界阻塞队列,其内部通过 ReentrantLock 和 Condition 实现线程安全和阻塞操作。该队列采用先进先出(FIFO)策略,并且容量固定,初始化后不可更改。在并发环境下,ArrayBlockingQueue 提供了高效的线程同步机制,适用于任务调度和生产者-消费者模型等场景。 #### 队列结构与成员变量 ArrayBlockingQueue 的内部结构由以下几个核心成员变量组成: - `items`:用于存储队列内部元素的数组。 - `takeIndex`:下一个 `take`、`poll`、`peek` 或 `remove` 操作的索引位置。 - `putIndex`:下一个 `put`、`offer`、`add` 操作的索引位置。 - `count`:当前队列中元素的数量。 - `lock`:所有访问入口的锁,基于 ReentrantLock 实现。 - `notEmpty`:出队操作的等待条件。 - `notFull`:入队操作的等待条件。 - `itrs`:当前活动迭代器的共享状态,如果没有,则为 null [^4]。 #### 构造函数与初始化 ArrayBlockingQueue 提供了两个构造函数: ```java public ArrayBlockingQueue(int capacity) { this(capacity, false); } public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) { throw new IllegalArgumentException(); } items = new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition(); } ``` 构造函数允许指定队列的容量以及是否使用公平锁。默认情况下,非公平锁用于提高吞吐量,而公平锁则确保线程按请求顺序获取锁,避免线程饥饿问题 [^2]。 #### 入队与出队操作 ArrayBlockingQueue 提供了多种入队和出队方法,包括阻塞和非阻塞两种方式: - `put(E e)`:阻塞式入队方法,若队列已满,线程将被挂起,直到队列有空闲空间。 - `offer(E e)`:非阻塞式入队方法,若队列已满,立即返回 false。 - `take()`:阻塞式出队方法,若队列为空,线程将被挂起,直到队列有元素可用。 - `poll()`:非阻塞式出队方法,若队列为空,立即返回 null。 以下是一个简单的入队操作示例: ```java ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue<>(3); for (int i = 0; i < 4; i++) { arrayBlockingQueue.add(i); } System.out.println("执行到了"); ``` 该示例中,当队列容量达到 3 时,调用 `add` 方法会抛出异常,因为 `add` 方法在队列满时不会阻塞,而是直接抛出 `IllegalStateException` [^1]。 #### 线程阻塞与唤醒机制 ArrayBlockingQueue 使用 ReentrantLock 和 Condition 实现线程的阻塞与唤醒。当调用 `put` 方法时,若队列已满,线程会被挂起并等待 `notFull` 条件信号。当有元素被取出后,`notFull` 条件被唤醒,等待的线程重新尝试入队操作。类似地,当调用 `take` 方法时,若队列为空,线程会被挂起并等待 `notEmpty` 条件信号。当有新元素被放入后,`notEmpty` 条件被唤醒,等待的线程重新尝试出队操作 [^4]。 #### 锁的公平性控制 ArrayBlockingQueue 支持公平锁和非公平锁两种模式。公平锁确保线程按照请求顺序获取锁,适用于对响应时间要求较高的场景。而非公平锁允许线程插队获取锁,从而提高吞吐量,适用于对性能要求较高的场景。通过构造函数的 `fair` 参数可以控制锁的公平性 [^5]。 #### 示例代码 以下是一个简单的 ArrayBlockingQueue 使用示例: ```java import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class ArrayBlockingQueueExample { public static void main(String[] args) { BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); Thread producer = new Thread(() -> { try { for (int i = 0; i < 20; i++) { String data = "Data-" + i; queue.put(data); System.out.println("Produced: " + data); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread consumer = new Thread(() -> { try { for (int i = 0; i < 20; i++) { String data = queue.take(); System.out.println("Consumed: " + data); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); producer.start(); consumer.start(); } } ``` 该示例展示了生产者-消费者模型,其中生产者线程将数据放入队列,消费者线程从队列中取出数据进行处理。 ###
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值