前面对Java中的锁进行了简单的分析,锁的使用和原理整体来说还是比较简单。今天我们来分析一下Condition这个类,这个类通常来说是跟Lock搭配使用的。比如说,如果一个线程获得了Lock的同步状态(即锁),但是由于达不到运行的条件,可能不能成功运行完毕,此时一种方式就是将它自己阻塞,等到条件满足再来重新运行。
本文的参考资料来源:
1.方腾飞、魏鹏、程晓明的《Java 并发编程的艺术》
2.Cay S.Horstmann的《Java 核心技术卷 I》
1.Condition的简单实用
我们还是先来说说我们的synchronized关键字吧,我们知道每个对象都有一组自己的监视器方法,从Object类继承过来的,主要包括wait方法和notify方法,这些方法与synchronized关键字配合使用的。在Condition接口上面,也提供了类似Object的监视器方法,与Lock配合使用。
我们来看看下面的例子:
public class ConditionUseCase {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void conditionWait() {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void conditionSignal(Thread thread) {
lock.lock();
try {
condition.signalAll();
} finally {
lock.unlock();
}
}
}
这里,我们可以看出来,Condition对象时从lock对象的newCondition创建的。同时,我们使用Condition的await方法来进行等待之前,必须获取获取lock的锁;同时如果一个线程被await方法阻塞了,我们可以通过Condition的signal方法来进行唤醒操作。
这里需要注意几个地方:
1.如果一个线程被一个Condition对象阻塞了,那么想要唤醒这个线程,必须调用同一个Condition对象的signal方法。我们可以这么来理解,一个线程被阻塞了,是阻塞在Condition对象上面的。
2.如果一个线程从Condition的await方法返回, 表示当前的线程已经获得了锁。这里先详细的解释一下线程await的过程:当一个线程调用Condition的await方法进行阻塞时,此时线程先将自己获取的锁释放了,此时将自己从同步队列里面取出来,并且添加到等待队列里面去,此时当前这个线程相当于阻塞这里了,不会往下执行;如果一个线程来调用这个Condition的signal方法,对阻塞在Condition的线程进行唤醒,此时被阻塞的线程从等待队列转移到同步队列,参与锁的竞争。从这里我们可以看出来如果一个线程被signal唤醒,不会直接从await方法返回,而是去参与锁的竞争,换句话说,如果一个线程从await方法出返回了,那么这个线程肯定是获得了锁的。还有一种情况,就是我们调用await方法来阻塞当前线程时,如果此时调用这个线程的intercept方法进行中断,线程不会立即抛出InterceptException异常,此时它去参与锁的竞争,只有获取到了锁才会抛出InterceptException异常。总之,如果一个线程从await方法返回,那么这个线程肯定获得了锁。
2.Condition的原理分析
Condition本身是一个接口,所以如果我们想要分析Condition的话,必须从它的实现类入手。我们先来看看ReentrantLock的newCondition方法返回的是什么东西。
final ConditionObject newCondition() {
return new ConditionObject();
}
我们发现,它返回的是一个ConditionObject对象。这个ConditionObject是AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部也是合理的。每个Condition对象都有这个一个队列,称为等待队列,该队列里面存储就是阻塞该Condition上面的线程。
现在我们来看看Condition。
(1).等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含一个线程引用,该线程就是被阻塞在Condition上面的线程。还记得我们在之前分析AbstractQueuedSynchronizer 的Node内部类,锁的同步队列存储的是每个Node,这里的等待队列存储的也是Node对象,其中Node有一个nextWaiter属性表示等待队列中下一个Node。我们来看看Condition的等待队列的结构图:
如图所示,Condition拥有一个firstWaiter对象,用来指向等待队列的队头,lastWaiter对象用来指向等待队列的队尾,而在更新队尾时,不用使用CAS来保证线程,因为在调用await方法时,该线程已经获得了锁,其他的线程已经被阻塞了。
我们再结合Condition的等待队列和AbstractQueuedSynchronizer的同步队列,构造出整个模型的结构图:
(2).等待过程
我们先来看看线程的等待过程,也就是线程调用await方法的过程。先来看看await方法的源代码:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//创建一个等待的Node,并且添加到等待队列中去
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);
}
从这个方法里面,我们可以看出来,整个等待过程分为3步:
1.创建一个等待Node,添加到等待当前Condition的等待队列中去。
2.当前线程释放锁。
3.当前线程进行阻塞。
其中addConditionWaiter方法进行第一步,fullyRelease方法进行第二步,LockSupport.park(this)方法进行第三步。是不是感觉非常的简单?现在我们来看看Condition的过程。
(3).通知过程
调用Condition的signal方法,将会唤醒在等待队列中的首节点,在唤醒之前,会将节点转移到AbstractQueuedSynchronizer的同步队列中。我们先来看看signal方法的源代码。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
从这个过程,我们可以简单的知道,如果一个线程想要调用signal方法的话,那么前提是这个线程获得了锁,因为这里调用了isHeldExclusively方法来进行检查;检查之后,就会调用都doSignal方法将当前这个节点转移到同步队列。
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
我们发现真正操作的是在transferForSignal方法里面,让我们来看看transferForSignal方法的源代码。
final boolean transferForSignal(Node 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;
}
我们从这个方法里面得出,整个唤醒过程分为3步:
1.首先,将Node的状态改为初始态。
2.状改变成功之后,将这个Node放入同步队列里面去。
3.最后,在唤醒线程。
网友评论