ArrayBlockingQueue
是常见的有界阻塞队列,用过线程池的,对它肯定不陌生。
它的实现原理,相信多数人也能说出个大概。可具体实现细节,估计会难住多数人。
我在网上查了一圈,大多是讲ArrayBlockingQueue怎么用的。它是怎么阻塞的,却少有人提及。
这里,由浅入深,我来剖析下ArrayBlockingQueue的源码,彻底弄懂它是怎么阻塞的。
很可能,这是你能看到的,最详细的源码解析
,不信的话,耐心看完,再和网上的比较下。
当然本人水平有限,若有论述不当之处,请大神指正。
ArrayBlockingQueue能干啥
ArrayBlockingQueue 底层是数组,容量是固定的。特别适用于,生产者-消费者模型。
也就是说,有线程往队列中放元素,有线程从队列中取元素,可保证线程安全。
用一句话概括:保证同一时刻,只有一个线程可操作队列
- 第一层理解:ArrayBlockingQueue 底层加锁,持有锁才能入队或出队操作,所以能保证线程安全。
- 第二层理解:入队操作,发现队满,会阻塞,当有出队操作时,队列中有了空位置,入队操作继续
当你看了本文的分析后,你会有更深入的理解:
入队、出队所竞争的是同一把锁。
从入队角度来说
当A线程持有锁,执行入队操作时,队满,A线程会到一个排队的地方,同时会释放锁。
释放的锁,假设被出队操作的线程B拿到。
在B线程执行完出队操作后,会释放锁。
但B线程释放锁之前,会把A线程从排队的地方给拽出来,放到抢锁的地方,让A有机会去抢锁。
当A抢到锁之后,此时一定有空位,则会继续执行入队操作。
入队结束后,A线程也会释放锁,但在释放之前,也会到排队的地方,找一个出队的线程,派去抢锁
这是我从源码中琢磨出来的,大概的流程,先有个印象。
一、ArrayBlockingQueue 的初始化
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
int len = 10;
Thread t0 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
for(int i = 0; i < len; i++){
queue.put("money");
log.info("线程:{},开始往队列里放钱了,这是第{}次放钱", Thread.currentThread().getName(), i);
}
}
},"t0");
Thread t1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
for(int i = 0; i < len; i++){
queue.take();
log.info("线程:{},取钱,取钱……这是第{}次取钱", Thread.currentThread().getName(), i);
}
}
},"t1");
t0.start();
Thread.sleep(3000);
t1.start();
}
本例中,我给了两个线程,一个往队列里放,一个从队列中取。
第1个线程启动后,sleep三秒,瞬间队满,而t1还未启动,t0会被阻塞。
当t1启动后,从队列中取,之后程序很快就执行完了。
这个可以自己跑一下,这里贴一个结果的图片。
/** 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;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
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();
}
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
测试用例中,执行这句代码时,源码层面,会初始化四个参数
- Object[] items — 队列底层的数组,此时大小固定了,不会再变
- ReentrantLock lock — 全局锁,操作队列需要先拿到这把锁
- Condition notFull — 队满了,还有入队操作,都在这儿等
- Condition notEmpty — 队空了,还有出队操作,都在这儿等
lock 底层是一个AQS对象, AQS里有个内部类ConditionObject。
notEmpty 和 notFull 就是ConditionObject的实例,用一张图来形象说明下。
二、入队源码浅析
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 获取锁,没拿到锁的阻塞在这行
try {
while (count == items.length)
notFull.await(); // 队满时操作
enqueue(e); // 入队
} finally {
lock.unlock(); // 释放锁
}
}
代码极其精简优雅,但里面的细节,极其复杂。
关于ReentrantLock,获取锁与释放锁,本文不讲。
但其涉及的源码的细节,必需要懂,要不这篇文章看越看越迷糊。
ReentrantLock 和 ArrayBlockingQueue 都是AQS
框架的应用,两者密切相关。
请先理解ReentrantLock,可以我之前的文章——ReentrantLock 源码详解。
本文重点解析 await() 和 enqueue(e)
先看入队的代码,很是精简
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0; // putIndex 指下次入队时,应放位置的下标
count++; // 队列中元素的个数
notEmpty.signal(); // 给出队的线程一个信号,队列中有元素了,可以取了。
}
enqueue()
这个方法很好理解,代码也很清晰,这里就不多说了,
signal()
这个方法后面再细说,这里先有个印象——发信号
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);
}
这个方法极其复杂,其中的逻辑不太好理解,先分步说明用意
1 addConditionWaiter()
:生成一个Node,并与线程绑定,放在notFull的等待队列里
2 fullyRelease()
:释放锁资源,即解锁
3 LockSupport.park(this)
:阻塞入队的线程,也就是在唤醒前,就停在这儿了
4 checkInterruptWhileWaiting()
: 若线程被中断,直接跳出 while 循环
5 acquireQueued(node, savedState)
:抢锁,抢不到就阻塞,直到抢到锁为止
6 unlinkCancelledWaiters()
:等待队列中,清除无效节点
7 reportInterruptAfterWait(int interruptMode)
:中断唤醒的节点抛出异常
看完是不很懵,其实我也很懵,之后想了好久,当想通那一刻,特高兴!
Doug Lea 真是大牛、大神,这么精巧的代码,怎么想出来的?
三、notFull.signal() 具体干了啥?
你想不明白notFull.await()
的代码逻辑,是因为notFull.signal()
具体做的啥,你不清楚。
别急,咱们用一个具体例子来说明这个问题,就用开头的示例代码。
假设刚开始,有五个线程 t1、t2、t3、t4、t5,顺次调用 put()方法,那前三个线程执行成功了。
t4拿到锁后,队满了,调用 notFull.await(),执行前3步后,阻塞了。
t5拿到锁后,同样是队满,调用 notFull.await(),执行前3步后,也阻塞了。
这时,假设线程 t6 来取钱,执行 take()
方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
此时count是3,直接走dequeue()
方法。
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.signal();
return x;
}
代码不复杂,直接看 notFull.signal()
public final void signal() {
if (!isHeldExclusively()) // 当前线程与持有锁的线程,不是同一个,抛出异常
throw new IllegalMonitorStateException();
Node first = firstWaiter; // 从 notFull 的等待队列中取第一个节点
if (first != null)
doSignal(first); // 结合上图,取到 线程t4那个结点
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null; // 如果原本的等待队列中只有一个线程,那取出一个线程后,将等待队列置空
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
transferForSignal(first) 这个方法,是将 first 放到抢锁的队列中,
即把 t4 放到sync队列中,若没有成功,就尝试放 t5。
插一句,生成等待队列中的note节点时,waitStatus 都是 -2,
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // ws由-2 设置为 0
return false; // ws设置为0失败,直接返回,外层进行下一个循环
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node); // 自旋方式将节点加到CLH队列尾部,并返回前驱节点
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread); // 前驱节点异常,直接唤醒当前节点
return true;
}
enq(node)
这个方法,在讲ReentrantLock 源码剖析时详细说过,这里就不再赘述。
t4 前驱节点,ws = 0,直接返回true, doSignal()
方法 do while 循环结束。 t6 线程释放锁。
如果你看过我之前写的 ReentrantLock 源码剖析,应该会知道,
锁资源释放时,会到CLH队列中,会唤醒头节点的后继节点,即唤醒 t4 。
至此 notFull.signal() 方法结束,总结——从等待队列中,捞出一个节点,放到CLH队列。
四、notFull.await() 源码详解
讲到这里,我们在大流程上,知道入队操作的原理,现在终于可以一行一行讲入队的源码了。先看notFull.await()
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);
}
1 addConditionWaiter()
:生成一个Node,并与线程绑定,放在notFull的等待队列里
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // 清除无效节点,代码有点复杂,在纸上画画就明白了
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
还用刚刚的t4 t5 例子,前面也画了图,入队操作,代码不复杂。
2 fullyRelease()
:释放锁资源,即解锁
ReentrantLock 源码剖析里讲过这个方法,此处不再赘述。
3 LockSupport.park(this)
:阻塞入队的线程,也就是在唤醒前,就停在这儿了
这一步是在 while 循环中,看循环条件,节点是否在 CLH 队列中。
若在就跳出循环。否则就进入循环,随即被阻塞。
前面讲 t6 线程时,说的很清楚, notFull.signal() 就是将节点放到CLH队列中。
也就是说,队满时,会阻塞在这一步,但凡有出队操作,就会将一个节点放到CLH队列中,
此时,阻塞的线程在被唤醒后,就可以跳出while循环。
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
*/
return findNodeFromTail(node);
}
/**
* Returns true if node is on sync queue by searching backwards from tail.
* Called only when needed by isOnSyncQueue.
* @return true if present
*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
4 checkInterruptWhileWaiting()
: 若线程被中断,直接跳出 while 循环
这个方法是处理中断的,不考虑中断,代码会容易理解。
初次接触,可假定没有中断,方便理解,若想深究,看源码。
这块代码的分析可以不用细看
private int checkInterruptWhileWaiting(Node node) {
// 如果线程有中断,park()方法就失效了,会进入这个方法
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
// 结合外层代码,只要被中断了,返回外层就不是0,一定会break,跳出while循环,最终被第7步处理
}
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node); // 入队成功,有机会去抢锁了。
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
while (!isOnSyncQueue(node))
Thread.yield(); // 入队失败,没在CLH队列中,歇下。
return false;
}
5 acquireQueued(node, savedState)
:抢锁,抢不到就阻塞,直到抢到锁为止
抢锁的源码,在ReentrantLock 源码剖析时详细说过。这里再简单说一下。
自旋抢锁,抢不到会被阻塞,等待唤醒之后再去抢。
什么时候抢到,什么时候结束这个方法。即这行代码之后的代码,都是持有锁资源的
6 unlinkCancelledWaiters()
:等待队列中,清除无效节点
这个前面提到过,源码不讲,自己在纸上画画。
7 reportInterruptAfterWait(int interruptMode)
:中断唤醒的节点抛出异常
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
这里中对中断的响应,进入CLH队列,是由于中断唤醒,非notFull.signal()唤醒,直接抛出异常。
至此 notFull.await() 方法讲解结束。总结下:
持有锁的线程,由于队满,无法执行入队,先释放锁,进入等待队列。
当有出队线程,执行出队操作时,会将等待队列中的节点,转移到CLH队列中
在CLH队列中的结点,阻塞的结点被唤醒,接着去抢锁,抢到锁后,执行后续操作
入队方法总结
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 抢到锁
try {
while (count == items.length) // 是否可入队
notFull.await(); // 释放锁,等到条件合适时,再抢到锁
enqueue(e); // 持有锁的情况下,执行入队操作
} finally {
lock.unlock(); // 释放锁资源
}
}
入队的方法很仔细的说完了,出队的方法,跟这个差不多的逻辑,这个懂了,那个就不用说了。
ArrayBlockingQueue
除了 put()
、 take()
方法之外,还有其它的方法,
比如入队的 offer(), add()、出队的 poll()
, peek()
, remove()
,等等。都差不多的套路。
好了,说到这个份儿上,为会么ArrayBlockingQueue
是线程安全的, 有界阻塞队列?
这个还是个事儿么?
相关文章: