分析 ArrayBlockingQueue 构造函数加锁问题

本文解析了ArrayBlockingQueue构造函数中使用锁的原因,旨在保证数组items的可见性,防止指令重排序导致的数据不一致问题。

转载自:https://blog.youkuaiyun.com/chenssy/article/details/78681423

  ArrayBlockingQueue 中的一个构造函数使用了锁:

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 {
        int i = 0;
        try {
            for (E e : c) {
                checkNotNull(e);
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            throw new IllegalArgumentException();
        }
        count = i;
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        lock.unlock();
    }
}

  上面的代码中获取互斥锁,锁的目的不是为了互斥,而是为了保证可见性。

  保证可见性?保证哪个可见性?我们知道 ArrayBlockingQueue 操作的其实就是一个 items 数组,这个数组是不具备线程安全的,所以保证可见性就是保证 items 的可见性。如果不加锁为什么就没法保证 items 的可见性呢?这其实是指令重排序的问题。
  为什么说指令重排序会影响 items 的可见性呢?创建一个对象要分为三个步骤:

  1. 分配内存空间;
  2. 初始化对象;
  3. 将内存空间的地址赋值给对应的引用。

  由于指令重排序的问题,步骤 2 和步骤 3 是可能发生重排序的。这个过程就会对上面产生影响。
  假如我们两个线程:线程 A负责ArrayBlockingQueue 的实例化工作,线程 B负责入队、出队操作。线程 A 优先执行,当它执行第 2 行代码,也就是this(capacity, fair);,如下:

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();
}

  注意,这里几个属性都是final修饰的,不会被重排序影响,执行完这个构造函数后,实际上实例已经完成了初始化的工作。如果之后的代码发生了重排序,即内存空间的地址赋值给对应的引用,但是c中的内容给items还没完成。
  那么对于线程B而言,ArrayBlockingQueue是已经完成初始化工作了,也就是可以使用了。其实线程A可能还正在执行构造函数中后面的代码。两个线程在不加锁的情况对一个不具备线程安全的数组同时操作,很有可能会引发线程安全问题。

  还有一种解释:缓存一致性。为了解决CPU处理速度以及读写主存速度不一致的问题,引入了 CPU 高速缓存。虽然解决了速度问题,但是也带来了缓存一致性的问题。在不加锁的前提下,线程A在构造函数中 items 进行操作,线程 B 通过入队、出队的方式对 items 进行操作,这个过程对 items 的操作结果有可能只存在各自线程的缓存中,并没有写入主存,这样肯定会造成数据不一致的情况。

<think>好的,我需要详细回答用户关于Java ArrayBlockingQueue实现原理的问题。首先,根据用户提供的引用资料,我需要整合相关信息,确保内容准确并符合要求。 首先,ArrayBlockingQueue是一个基于数组的有界阻塞队列,属于java.util.concurrent包,适用于生产者-消费者模型。根据引用[1][2][3],它使用ReentrantLock来实现线程安全,并且内部有一个定长数组存储元素。我需要解释它的核心组件:数组、读写指针、锁和条件变量。 接下来,线程安全机制部分,应该详细说明它如何通过ReentrantLock和两个Condition(notEmpty和notFull)来控制并发访问。引用[1]提到需要了解ReentrantLock的原理,这里可以结合锁的公平性设置,如引用[2]中的内容,说明默认非公平但可配置为公平锁。 然后,阻塞机制部分,需要描述当队列满或空时,如何通过await()和signal()方法让线程等待或被唤醒。这部分可以结合引用[3]的例子,说明生产者消费者如何通过put()和take()方法操作队列。 生产消费流程部分,需要分步骤说明put和take的具体操作,包括加锁、检查队列状态、阻塞或插入/取出元素、释放锁等。引用[4]提到了线程池中任务队列的使用,可以联系起来说明ArrayBlockingQueue的实际应用场景。 线程池中的应用部分,根据引用[4][5],可以提到Executors创建的线程池如newFixedThreadPool使用LinkedBlockingQueue,但ArrayBlockingQueue同样适用于需要固定大小的队列场景。这里需要区分两者的不同,并强调ArrayBlockingQueue的有界特性。 最后,性能特点部分,要说明其高吞吐和低延迟的优势,以及锁竞争可能带来的性能影响,引用[2]提到的最佳实践可能涉及如何合理设置队列大小和锁策略。 在组织内容时,要确保使用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$。同时,生成的相关问题要符合用户提供的示例结构,涵盖实现原理、锁机制、区别、应用场景和优化等方面。 检查是否有遗漏的引用内容,确保每个引用标识正确对应,如引用[1]到[5]的正确使用。最后,保持回答结构清晰,分点明确,语言简洁,符合用户要求的中文回答。</think>### Java ArrayBlockingQueue 实现原理详解 #### 1. **核心组件** **ArrayBlockingQueue** 是基于数组实现的有界阻塞队列,其核心组件包括: - **定长数组**:用于存储队列元素,容量在初始化时确定且不可变[^2]。 - **读写指针**:使用 `putIndex` 和 `takeIndex` 分别标记生产者插入位置和消费者取出位置,循环复用数组空间[^2]。 - **锁机制**:通过 `ReentrantLock` 保证线程安全,默认采用非公平锁(可通过构造函数选择公平锁)[^1]。 - **条件变量**:通过 `Condition` 对象 `notEmpty` 和 `notFull` 实现阻塞/唤醒机制[^3]。 #### 2. **线程安全机制** - **互斥访问**:所有入队(如 `put()`)和出队(如 `take()`)操作需先获取唯一的 `ReentrantLock`,确保同一时刻只有一个线程操作队列[^1]。 - **阻塞控制**: - **队列满时**:生产者调用 `put()` 会触发 `notFull.await()` 进入阻塞状态,直到有消费者取出元素后通过 `notFull.signal()` 唤醒。 - **队列空时**:消费者调用 `take()` 会触发 `notEmpty.await()` 阻塞,直到生产者插入元素后通过 `notEmpty.signal()` 唤醒。 #### 3. **生产与消费流程** 以 `put(E e)` 和 `take()` 方法为例: ```java // 生产者操作 public void put(E e) throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) // 队列已满 notFull.await(); // 等待非满条件 enqueue(e); // 插入元素并更新putIndex } finally { lock.unlock(); } } // 消费者操作 public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) // 队列为空 notEmpty.await(); // 等待非空条件 return dequeue(); // 取出元素并更新takeIndex } finally { lock.unlock(); } } ``` #### 4. **性能特点** - **高吞吐**:基于数组的连续内存访问特性,缓存命中率高。 - **锁竞争**:单锁设计可能导致高并发场景下性能下降,但简化了实现复杂度。 - **有界性**:避免无限制内存增长,适用于资源受限的系统。 #### 5. **在线程池中的应用** `ArrayBlockingQueue` 常用于线程池的任务队列,例如: ```java // 创建固定大小的线程池,使用ArrayBlockingQueue作为任务队列 ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100) // 容量为100的有界队列 ); ``` 其有界特性可防止任务堆积导致内存溢出,同时通过阻塞机制平衡生产者和消费者的速度[^4][^5]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值