ArrayBlockingQueue类结构

一、ArrayBlockingQueue类结构

先看一下ArrayBlockingQueue类里面有哪些属性:

  public class ArrayBlockingQueue<E>
          extends AbstractQueue<E>
          implements BlockingQueue<E>, java.io.Serializable {


      /**
       * 用来存放数据的数组
       */
      final Object[] items;

      /**
       * 下次取数据的数组下标位置
       */
      int takeIndex;

      /**
       * 下次放数据的数组下标位置
       */
      int putIndex;

      /**
       * 元素个数
       */
      int count;

      /**
       * 独占锁,用来保证存取数据安全
       */
      final ReentrantLock lock;

      /**
       * 条件队列,如果数组为空,就一直阻塞 take 取数据的线程,直到被唤醒
       */
      private final Condition notEmpty;

      /**
       * 条件队列,如果队列已满,就一直阻塞 put 存数据的线程,直到被唤醒
       */
      private final Condition notFull;

  }

根据成员变量的介绍可以得知:

  • ArrayBlockingQueue底层是基于数组实现的,使用对象数组 items 存储元素

  • takeIndex:表示下次取数据的位置,putIndex:表示下次放数据的位置

  • ArrayBlockingQueue 还使用 ReentrantLock 保证线程安全,并且定义了两个条件,当条件满足的时候才允许放数据 或 者取数据


二、初始化

ArrayBlockingQueue常用的初始化方法有两个:

  • 指定容量大小

  • 指定容量大小和是否是公平锁

       /**
        * 指定容量大小的构造方法
        */
       BlockingQueue<Integer> blockingDeque1 = new ArrayBlockingQueue<>(1);
    
       /**
        * 指定容量大小、公平锁的构造方法
        */
       BlockingQueue<Integer> blockingDeque1 = new ArrayBlockingQueue<>(1, true);
    

再看一下对应的源码实现:

/**
 * 指定容量大小的构造方法(默认是非公平锁)
 */
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}


/**
 * 指定容量大小、公平锁的构造方法
 *
 * @param capacity 数组容量
 * @param fair     是否是公平锁
 */
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0) {
        throw new IllegalArgumentException();
    }
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    // 如果数组为空,就一直阻塞 take 取数据的线程,直到被唤醒
    notEmpty = lock.newCondition();
    // 如果队列已满,就一直阻塞 put 存数据的线程,直到被唤醒
    notFull = lock.newCondition();
}

原理图流程分析:


三、放数据源码

放数据的方法有四个:


四、入队方法


1、boolean add(E e)

调用父类 add 方法,而父类 AbstractQueue add 方法实际调用的是 offer(E e) 方法

public boolean add(E e) {
    return super.add(e);
}

// super.add 父类的add
public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}


2、boolean offer(E e)

加锁后判断容量大小,满了就不添加;没满执行共用的入队方法,入队成功返回 true

/**
* 插入成功返回 true,否则返回 false
* 插入元素为 NULL 返回空指针异常
*/
public boolean offer(E e) {
    checkNotNull(e); // 首先判断元素是否为 NULL,为 NULL 抛出异常
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count == items.length)
            return false; // 若数组满,返回 false
        else {
            enqueue(e); // 添加元素并返回 true
            return true;
        }
    } finally {
        lock.unlock();
    }
}


3、 void put(E e) throws InterruptedException

put()方法在数组满的时候,会一直阻塞,直到有其他线程取走数据,空出位置,才能添加成功

public void put(E e) throws InterruptedException {
    // 1. 判空,传参不允许为null
    checkNotNull(e);
    // 2. 加可中断的锁,防止一直阻塞
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 3. 如果队列已满,就一直阻塞,直到被唤醒
        while (count == items.length) {
            notFull.await();
        }
        // 4. 如果队列未满,直接入队
        enqueue(e);
    } finally {
        // 5. 释放锁
        lock.unlock();
    }
}


4、enqueue()

  • 仅在同步方法块中调用

  • putIndex 的值一直在从 0 到 items.length 之间循环,添加元素进来就往后移动一位,移动到队尾就又回到了开头,像是在环形数组上面移动

     private void enqueue(E x) {
    
         // 1. 获取数组
         final Object[] items = this.items;
         // 2. 直接放入数组
         items[putIndex] = x;
         // 3. 移动putIndex位置,如果到达数组的末尾就从头开始
         if (++putIndex == items.length) {
             putIndex = 0;
         }
         // 4. 计数
         count++;
         // 5. 唤醒因为队列为空,而阻塞等待take取数据的线程,表示当前数组有元素可取
         notEmpty.signal();
     }
    


四、取数据源码

取数据源(取出数据并删除)的方法有四个:


1、 remove方法源码

remove()方法源码,如果数组为空,remove()会抛出异常

/**
 * remove方法入口
 */
public E remove() {
    // 1. 直接调用poll方法
    E x = poll();
    // 2. 如果取到数据,直接返回,否则抛出异常
    if (x != null) {
        return x;
    } else {
        throw new NoSuchElementException();
    }
}


2、 poll方法源码

poll()方法在弹出元素的时候,如果数组为空,则返回null,表示弹出失败

/**
 * poll方法入口
 */
public E poll() {
    // 1. 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 2. 如果数组为空,则返回null,否则返回队列头部元素
        return (count == 0) ? null : dequeue();
    } finally {
        // 3. 释放锁
        lock.unlock();
    }
} 


3、take方法源码

take()方法源码,如果数组为空,take()方法就一直阻塞,直到被唤醒

/**
 * take方法入口
 */
public E take() throws InterruptedException {
    // 1. 加可中断的锁,防止一直阻塞
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 2. 如果数组为空,就一直阻塞,直到被唤醒
        while (count == 0) {
            notEmpty.await();
        }
        // 3. 如果数组不为空,就从数组中取数据
        return dequeue();
    } finally {
        // 4. 释放锁
        lock.unlock();
    }
}


4、 dequeue 内部共用的出队操作

/**
 * 出列
 */
private E dequeue() {

    // 1. 取出队列头部元素
    final Object[] items = this.items;
    E x = (E) items[takeIndex];

    // 2. 取出元素后,把该位置置空
    items[takeIndex] = null;

    // 3. takeIndex 往后移动一位,如果到达数组的末尾,那么让他回到开头
    if (++takeIndex == items.length) {
        takeIndex = 0;
    }
    // 4. 元素个数减一
    count--;
    if (itrs != null) {
        itrs.elementDequeued();
    }
    // 5. 唤醒因为队列已满,而被阻塞等待存放数据的线程
    notFull.signal();
    return x;
}


五、查看数据源码

再看一下查看数据源码,查看数据,并不删除数据


1、 peek方法源码

peek()方法源码,如果数组为空,就返回 null

  /**
   * peek方法入口
   */
  public E peek() {
      // 1. 加锁
      final ReentrantLock lock = this.lock;
      lock.lock();
      try {
          // 2. 返回数组头部元素,如果数组为空,则返回null
          return itemAt(takeIndex);
      } finally {
          // 3. 释放锁
          lock.unlock();
      }
  }

  /**
   * 返回当前位置元素
   */
  final E itemAt(int i) {
      return (E) items[i];
  }


2、element方法源码

element()方法源码,如果数组为空,则抛出异常

/**
 * element方法入口
 */
public E element() {
    // 1. 调用peek方法查询数据
    E x = peek();
    // 2. 如果查到数据,直接返回
    if (x != null) {
        return x;
    } else {
        // 3. 如果没找到,则抛出异常
        throw new NoSuchElementException();
    }
}


六、总结

ArrayBlockingQueue队列具有以下特点:

1、ArrayBlockingQueue实现了BlockingQueue接口,提供了四组放数据和读数据的方法,来满足不同的场景

2、ArrayBlockingQueue底层基于数组实现,采用循环数组,提升了数组的空间利用率

3、ArrayBlockingQueue初始化的时候,必须指定队列长度,是有界的阻塞队列,所以要预估好队列长度,保证生产者和消费者速率相匹配

4、ArrayBlockingQueue的方法是线程安全的,使用ReentrantLock在操作前后加锁来保证线程安全

posted @ 2025-05-28 03:23  jock_javaEE  阅读(25)  评论(0)    收藏  举报