ArrayBlockingQueue的put和take过程详解

ArrayBlockingQueue具有以下特性:

1)队列是有边界的,在创建时需指定队列大小;

2)队列是先进先出,从尾部进,从头部出;

3)队列已满时会阻塞添加;队列为空时会阻塞获取;

4)支持以公平和非公平的方式,写入和获取元素。

下面从源码的角度来分析下ArrayBlockingQueue的代码实现,也就能够清晰明白ArrayBlockingQueue的特性,主要从put和take过程讲解。

创建

ArrayBlockingQueue提供了3个构造方法:

// 指定容量,默认非公平
ArrayBlockingQueue(int capacity)
ArrayBlockingQueue(int capacity, boolean fair)
// 指定一个初始内容
ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c)

另外两个构造方法底层都是调的这个,

public ArrayBlockingQueue(int capacity, boolean fair) {
  if (capacity <= 0)
    throw new IllegalArgumentException();
  // 创建一个对象数组
  this.items = new Object[capacity];
  // 创建一个lock
  lock = new ReentrantLock(fair);
  // 用于阻塞take
  notEmpty = lock.newCondition();
  // 用于阻塞put
  notFull =  lock.newCondition();
}

ReentrantLock的加锁解锁讲解,参考文章:ReentrantLock详解

put过程

先来看一个例子,根据这个例子来看下执行过程。

public class TestBlockQueue {

    public static void main(String[] args) {
        final ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10);
        final AtomicInteger atomicInteger = new AtomicInteger();
        Runnable runnable = () -> {
            while (true) {
                try {
                    sleep(1000L);
                    int i = atomicInteger.incrementAndGet();
                    blockingQueue.put(i);
                    System.out.println("put数字 " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t0 = new Thread(runnable);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        Thread t3 = new Thread(runnable);
        t0.start();
        t1.start();
        t2.start();
    }

    private static void sleep(Long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            // ignore
        }
    }
}

输出:

Thread-2 put数字 3
Thread-0 put数字 1
Thread-1 put数字 2
Thread-2 put数字 4
Thread-0 put数字 6
Thread-1 put数字 5
Thread-0 put数字 7
Thread-2 put数字 8
Thread-1 put数字 9
Thread-2 put数字 11

程序输出了这10个数字,就卡住了,不会继续put内容了。而且数字10,并没有输出,直接输出了11。数字10被阻塞在哪了?

先看下put代码:

public void put(E e) throws InterruptedException {
  checkNotNull(e);
  final ReentrantLock lock = this.lock;
  // 锁定
  lock.lockInterruptibly();
  try {
    while (count == items.length)
      // 元素已加满,阻塞,将线程放在条件队列
      notFull.await();
    // 元素添加到queue
    enqueue(e);
  } finally {
    lock.unlock();
  }
}

public final void await() throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  // 加入条件队列
  Node node = addConditionWaiter();
  // 释放锁
  int savedState = fullyRelease(node);
  int interruptMode = 0;
  while (!isOnSyncQueue(node)) {
    // 阻塞线程
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
      break;
  }
  if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
  if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
  if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);
}

因此在加满元素时,线程阻塞在条件队列里,如图:
image-20220507081709013

条件队列是单向的,不像ReentrantLock里的同步队列(ReentrantLock详解)是双向的。

总结下进条件队列过程:

1)元素加满了,调用notFull.await();

2)执行addConditionWaiter(),入条件队列

3)fullyRelease(node),释放当前线程占用的锁

4)线程阻塞,LockSupport.park(this)

take过程

在上面的例子里再加入线程进行获取元素:

Runnable takeRunnable = () -> {
  while (true) {
    try {
      sleep(1000L);
      blockingQueue.take();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
};
Thread t3 = new Thread(takeRunnable);
Thread t4 = new Thread(takeRunnable);
Thread t5 = new Thread(takeRunnable);
t3.start();
t4.start();
t5.start();

先看下take代码:

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  // 获取锁
  lock.lockInterruptibly();
  try {
    while (count == 0)
      // 元素为空,阻塞,将线程放在条件队列
      notEmpty.await();
    // 元素从queue出队
    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];
  items[takeIndex] = null;
  if (++takeIndex == items.length)
    takeIndex = 0;
  count--;
  if (itrs != null)
    itrs.elementDequeued();
  // 给notFull条件队列发一个信号
  notFull.signal();
  return x;
}

我们假设t3是第一个获取到锁的线程,并且执行完notFull.signal(),还未释放锁,这个时候,ArrayBlockingQueue对象里面发生了什么?

1)取出第一个元素,takeIndex+1
image-20220507101945462

2)执行notFull.signal(),将条件队列的第一个出列,并加入到lock的同步队列
image-20220507104004522

3)如果t4、t5线程已经开始执行,并在等待t3释放锁
image-20220507104106754

4)当t3释放锁,因为t4、t5会取数,同样会把t0、t2,加入到同步队列

5)当items没有内容时,会执行notEmpty.await(),阻塞取数线程。notEmpty也是一个条件队列,会存储取数的线程(t3、t4、t5),跟notFull类似。

总结

1)定义一个数组items,存储内容;

2)使用ReentrantLock,控制安全的存储和读取,并可实现公平和非公平;

3)使用两个条件队列notFull和notEmpty,来控制数组满和空的情况下,阻塞线程;

4)put或take快结束时,使用notEmpty和notFull的signal方法,把阻塞在条件队列的node转移到同步队列,以便可以继续读取或写入数据。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值