Condition是Lock接口中实现像Object类中的wait()和notify()等的一个方式,前面我们只说到了如何进行lock()和unlock()以及里边的原理,那我们来说下使用AQS的基础下,如何实现阻塞释放锁。
Condition接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
| 对比项 | Object Monitor Methods | Condition |
|---|---|---|
| 前置条件 | 获取对象的锁 | 调用Lock.lock()获取锁调用Lock.newCondition()获取Condition对象 |
| 调用方式 | 直接调用 如:object.wait() | 直接调用 如:condition.await() |
| 等待队列个数 | 一个 | 多个 |
| 当前线程释放锁并进入等待状态到将来的某个时间 | 不支持 | 支持 |
| 当前线程释放锁并进入等待状态,在等待状态中不响应中断 | 不支持 | 支持 |
Condition是依赖Lock对象的,一般都会将Condition作为成员变量。
Condition的实现分析
ConditionObject是同步器AQS的内部类,每个Condition对象都包含着一个队列(等待队列),该队列是Condition对象实现等待/通知功能的关键。
等待队列
- 等待队列是一个FIFO队列,在队列中的每个节点都包含一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await(),那么该线程将会释放锁,构造成节点加入等待队列并进入等待状态。
- 等待队列中的节点和同步队列的节点类型都是AQS的静态内部类AbstractQueuedSynchroniezer.Node。
- 一个Condition包含一个等待队列,Condition拥有首节点和尾节点,当线程调用Condition.await()方法,将会以当前线程构成节点,并将节点从尾部加入等待队列
- 上述更新过程无需CAS保证,因为调用await()的时候一定是获取了锁
- Lock可以拥有一个同步队列和多个等待队列
- Condition的实现是同步器的内部类,因此每个Condition实例都能访问同步器提供的方法
等待
- 调用Condition的await()方法,会使得当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁
- 调用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);
}
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列。
public final void signal() {
if (!isHeldExclusively())//表示是否被当前状态占用
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
调用该方法的前置条件是当前线程必须获取了锁,接着在获取等待队列的第一个节点,再将其移动到同步队列并使用LockSupport唤醒节点中的线程。
- 通过调用AQS的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列中,当前线程再使用LockSupport唤醒该节点的线程。
- 被唤醒的线程将从await()方法中的while循环中退出,进而调用同步器acquireQueued()方法加入到获取同步状态的竞争中
- 成功获取同步状态以后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。
小结
一般在获取到锁的时候,才会将其加入到等待队列(队尾)中,也就是进行阻塞,当调用其中的一个Condition的signal方法的时候,会唤醒当前调用signal()方法的Condition对象(等待队列)的队头节点,并将其加入到同步队列的队尾,使其能过够进行自旋判断自己是否获取同步状态
一个打印100次ABCABCABC的例子
ReentrantLock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
Thread A =new Thread(){
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 100; i++) {
while (state % 3 != 0) {
conditionA.await();
}
state++;
System.out.print("A");
//当A执行完以后 要去唤醒B线程
conditionB.signal();
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
};
Thread B =new Thread(){
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 100; i++) {
while (state % 3 != 1) {
conditionB.await();
}
state++;
System.out.print("B");
//当B执行完以后 要去唤醒C线程
conditionC.signal();
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
};
Thread C =new Thread(){
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 100; i++) {
while (state % 3 != 2) {
conditionC.await();
}
state++;
System.out.print("C");
//当C执行完以后 要去唤醒A线程
conditionA.signal();
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
};
A.start();
C.start();
B.start();