Condition详解
JDK的Lock是为了替代synchronized同步锁,Condition是为了替代Object的对象监视器锁。
利用Condition,每个对象可以有多个不同的等待集(wait-sets),这样多个线程可以分别在同一个对象的多个等待集上等待,相对于Object的对象监视器锁,Condition提供了更加精细化的锁操作。
Condition(也可以称为Condition queue,条件队列),如果当前线程在某个条件上等待,当该条件满足时,其他线程可以唤醒该线程,该线程可以继续去竞争锁。
Condition和Lock是绑定的,一个Lock可以关联多个Condition,通过Lock.newCondition()方法获得一个Condition。
Condition的使用
举例说明Conditon的使用:
设想我们现在有一个有界的缓存BoundedBuffer,该缓存提供了put和take方法。如果在一个空的BoundedBuffer上take数据,当前线程将会阻塞,直到该缓存有一项数据可用。如果在一个满的BoundedBuffer上put数据,当前线程也会阻塞,直到该缓存有一个空闲位置可用。通过使用两个Condition,可以分别让put和take的线程在不同的条件下等待,这样我们可以优化通知机制,当缓存不空(或者不满)的时候,只通知那些需要take(或者put)数据的线程,避免了无效通知。
看下BoundedBuffer的实现:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* <p>文件描述: 有界缓存</p>
*
* @Author luanmousheng
* @Date 17/8/13 上午10:02
*/
public class BoundedBuffer {
private final Lock lock = new ReentrantLock();
//锁lock上的两个条件
//缓存不满条件
private final Condition notFull = lock.newCondition();
//缓存不空条件
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[100];
private int putptr = 0;
private int takeptr = 0;
private int count = 0;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
//等待缓存不满
notFull.await();
}
items[putptr] = item;
if (++putptr == items.length) {
putptr = 0;
}
count++;
//缓存不空,其他取数据阻塞的线程可以被唤醒了
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
//等待缓存不空
notEmpty.await();
}
Object res = items[takeptr];
if (++takeptr == items.length) {
takeptr = 0;
}
count--;
//缓存不满,其他添加数据阻塞的线程可以被唤醒了
notFull.signal();
return res;
} finally {
lock.unlock();
}
}
}
Condition接口定义
Condition是一个接口,定义了条件需要实现的语义。
public interface Condition {
/**
响应中断的条件等待
*/
void await() throws InterruptedException;
/**
不响应中断的条件等待
*/
void awaitUninterruptibly();
/**
带超时时间的等待,单位:纳秒
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
带超时时间的等待
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
带到期时间的等待
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
唤醒某个在该条件上等待的线程
*/
void signal();
/**
唤醒所有在该条件上等待的线程
*/
void signalAll();
}
Condition的实现
Condition也叫条件队列,AbstractQueuedSynchronizer实现了该接口。以前学习ReentrantLock的时候也说到了AQS,它是JDK锁和并发的核心实现。AQS维护了一个等待队列,如果锁关联了n个Condition,AQS还会维护n个条件队列,队列的节点都是AQS的内部静态类Node。
AQS的内部类ConditionObject实现了Condition接口,看下ConditionObject的定义:
public class ConditionObject implements Condition, java.io.Serializable {
/**
条件队列的第一个节点
*/
private transient Node firstWaiter;
/**
条件队列的最后一个节点
*/
private transient Node lastWaiter;
//……
}
ConditionObject是通过firstWaiter和lastWaiter维护了一个条件队列。
通常我们会使用条件队列的await和signal方法,那我们就首先学习await的实现原理:
await方法
public final void await() throws InterruptedException {
//await方法通过抛出中断异常响应中断
if (Thread.interrupted())
throw new InterruptedException();
//包装当前线程,加入到条件队列的队尾
Node node = addConditionWaiter();
//线程阻塞前释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断该节点是否在等待队列上,如果在等待队列,说明该节点已经被其他线程唤醒并移到了等待队列上
while (!isOnSyncQueue(node)) {
//不在等待队列,继续在条件队列阻塞
LockSupport.park(this);
//interruptMode!=0,说明在该线程唤醒前或者唤醒后被中断过
//响应中断,跳出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//在等待队列排队等待获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//可能存在被取消的节点,删除这些被取消的节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//被中断过,响应中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await方法主要做了这么几件事:
- 将当前线程加入到条件队列的队尾。
- 释放当前线程已经获取的锁。
- 自旋检查当前节点是否在等待队列,如果不是则继续挂起,否则说明有其他线程唤醒了该线程,尝试去等待队列获取锁。
- 被唤醒或者被中断后,在等待队列中排队获取锁。
await主要流程就是这样,让我们再看下详细实现,看下addConditionWaiter方法:
private Node addConditionWaiter() {
Node t = lastWaiter;
//如果条件队列队尾的状态不是CONDITION,可能存在已经取消的节点,删除这些节点
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;
}
fullyRelease释放当前线程持有的锁。
isOnSyncQueue判断该节点是否在等待队列:
final boolean isOnSyncQueue(Node node) {
//结点状态为CONDITION肯定不在等待队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//存在next节点,说明node肯定在等待队列中
if (node.next != null)
return true;
//从尾往头遍历查找
return findNodeFromTail(node);
}
acquireQueued之前在讲ReentrantLock时已经说明过了,这里不再讲解。
signal方法
signal方法唤醒条件队列上某个节点,并将该节点从条件队列移到等待队列。
public final void signal() {
//当前线程持有锁才能唤醒其他线程,否则抛出监视器异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
//条件队列不为空
if (first != null)
//主要逻辑都在这里
doSignal(first);
}
这断代码的重点是doSignal方法:
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
doSignal方法遍历条件队列,直到找到第一个未取消的节点。找到第一个未取消的节点后将该节点移到等待队列,然后退出该方法。
看下transferForSignal方法:
//将节点从条件队列移到等待队列
final boolean transferForSignal(Node node) {
//原子的更新waitStatus为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将该节点添加到等待队列
//返回当前节点的前置节点
Node p = enq(node);
int ws = p.waitStatus;
//如果前置节点已取消或者设置状态为SIGNAL失败,则唤醒当前节点线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
该方法将节点从条件队列移到等待队列。
signalAll方法
该方法唤醒条件队列中的所有节点线程,并将这些节点从条件队列移到等待队列。
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
主要看下doSignalAll方法:
//将first及其之后的节点从条件队列移到等待队列
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
//GC
first.nextWaiter = null;
//将节点移到等待队列
transferForSignal(first);
first = next;
//当节点不为空时,转移节点
} while (first != null);
}
这断代码逻辑比较简单,遍历条件队列,调用transferForSignal方法,将节点从条件队列移到等待队列。
以上就是条件队列的主要逻辑。