Condition的使用
假设需要实现一个有界队列,在添加元素时,如果队列已满,则需要等待队列有空位时才能添加;若队列为空,则需要等待队列有元素时才能删除。
对于这种需求,仅使用Lock就会有点捉襟见肘了。为此,可选择使用Condition来解决这个问题。下面是相关的Demo:
public class BoundedQueue<T> {
private Object[] items;
private int addIndex, removeIndex, count;
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count >= items.length) {
notFull.await();
}
items[addIndex] = t;
addIndex++;
if (addIndex == items.length) {
addIndex = 0;
}
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
Object removeItem = items[removeIndex];
removeIndex++;
if (removeIndex == items.length) {
removeIndex = 0;
}
count--;
notFull.signal();
return (T) removeItem;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
BoundedQueue<String> queue = new BoundedQueue<>(1);
new Thread(() -> {
try {
System.out.println("remove await ");
countDownLatch.await();
for (;;) {
final String x = queue.remove();
System.out.println("remove item: " + x);
if (StringUtil.isNullOrEmpty(x)) {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(2000);
new Thread(() -> {
try {
countDownLatch.countDown();
System.out.println("add item: hello");
queue.add("hello");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
countDownLatch.countDown();
System.out.println("add item: world");
queue.add("world");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
try {
System.out.println("add item: null");
queue.add(null);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
用法和object.wait和object.notify是非常相似的。其区别在于,一个锁可对应多个condition,即可以有多个等待队列与锁对应。而使用object的wait和notify时,一个锁只能有一个等待队列与之对应。相比之下,condition的await和signal相对灵活许多。
那Condition要如何实现才能达到上述效果呢?
为方便叙述,假设现存一个AQS,其中的结构为:
每个节点都是以独占模式的方式存在于AQS中。
等待队列的创建
要使用Condition,首先要创建一个等待队列。创建等待队列的方式很简单,只需要new出来即可
Condition condition = lock.newCondition();
对应地,在AQS中,就会创建了一个ConditionObject的对象。在这个ConditionObject中,有firstWaiter和lastWaiter,它们分别指向等待队列中头结点和尾结点。现在刚创建出来,它们现在的指向肯定是null。
在等待队列中等待
new出来之后如何使用呢?只需要调用await方法,即可让线程处于等待队列中。
condition.await();
如图所示,在调用await方法时,实际上是做了以下几件事
- 先将当前线程构造出一个新的Node,此处命名为 Node_Waiter1,然后进入等待队列。注意这里并没有复用原有的Node1;
- 调用release方法释放锁并唤醒同步队列的后继线程,这样就会让处于同步队列队头位置的Node1出队,并且Node2也会被唤醒;这么做的目的,是因为此时Node1所代表的线程需要等待某些条件成立才能继续进行,但是后继线程并不需要因为它的前继结点处于等待而继续被阻塞,而且此时锁也被释放了,所以此时后继结点的线程需要被唤醒。
- Node1出队后,由第1步构造的 Node_Waiter1 就会调用
LockSupport.park(this)方法,让当前线程进入BLOKED状态。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 对应第1步,构造新结点,加入等待队列
Node node = addConditionWaiter();
// 对应第2步,释放锁,唤醒同步队列中的后继结点
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 对应第3步,让调用线程进入阻塞状态。注意这里的node变量并不是指Node1,而是出于等待队列中的Node_Waiter1。
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 一旦这个新的Node_Waiter1结点被唤醒,它就会加入到同步队列,通过acquireQueued方法和其他线程排队竞争同步锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
需要注意:
- 当调用await方法,意味着该线程会释放锁,其他线程会获得锁。也就是说,Condition相关的方法,都要在成功获得锁的情况下调用。
- 正因为Condition是在获得锁的情况下才能调用await,在独占模式下,获得锁的线程只有一个,所有操作都是单线程的,所以在waiter结点加入等待队列时,都没有采取保证线程安全的处理。
唤醒等待队列中的结点
处于等待队列中的结点,当条件成立时,可以通过signal方法被唤醒。
condition.signal();
当执行了signal方法后,原本在等待队列的结点会被移出等待队列,接着摇身一变,加入同步队列,成为其中一员,继续内卷。其过程大致如图所示:
signal相关的代码如下所示
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
核心方法是transferForSignal,在这个方法中,会将等待结点加入到同步队列中,并将状态从CONDITION改为SIGNAL。注意这里只是让node.thread进入同步队列,并没有去竞争同步锁,而是在唤醒线程,在await方法中调用acquireQueued方法后才去竞争同步锁。而acquireQueued方法,是在调用await方法的时候调用的。
总结时刻
你需要记住两大方面:
1. Condition的使用
和object.wait和notify类似,都需要将await和signal放到一个循环中进行调用,目的是为了防止线程不满足业务上的条件但被意外唤醒。
2. Condition的原理
- 本质上还是使用单向链表构造的队列,每个Condition对象都是一个队列,需要在获取到锁之后才能使用Condition。
- await表示原本变成新结点入队,已经持有的锁要释放,因为当前线程阻塞在了Condition上,不需要再阻塞其他线程获取锁,同时也因为其他线程需要获取到锁才能调用condition.signal()
- signal表示等待队列中的头结点出队,接着加入到同步队列中,和其他线程竞争锁。