背景
我们在谈到多线程并发访问临界资源的时候,通常会想到Object类的monitor锁和wait/notify(条件等待/通知)操作。很多同学会想问,既然有了加锁/解锁机制,为什么还要引入条件等待/通知操作呢?原因在于,当A线程对临界资源加锁后(排他锁),其他线程将无法再次获取该临界资源,直到A线程将锁释放。如果A线程执行很久,那么其他线程将在很长一段时间内无法持有临界资源的锁。但是对于Object.wait,A线程将放弃锁,进入等待池,等其他线程调用Object.notifyAll的时候,对它进行唤醒,然后再次竞争锁。因此,在实际工程使用中,如果A线程持有锁之后,需要等待其他条件时,通常会采用将其加入条件等待队列的方式,放弃锁,等待其他线程通知它恢复运行。
锁与条件等待机制的实现方式
锁与条件等待机制的实现方式主要有两种:基于Object的方式和基于ReentrantLock的方式。
基于Object的方式
这种方式下,是围绕Object类展开的。
使用synchronized关键字,本质上是使用Object的monitor锁提供锁的机制。
使用Object的wait和notify/notifyAll方法,通过JVM底层实现了条件等待队列和通知机制。
基于可重入锁的方式
这种方式主要是围绕着ReentrantLock展开的。
在之前的文章中我们简单介绍了可重入锁:ReentrantLock,其可重入性解决synchronized不可重入的问题,其采用state状态来维护是否有线程持有锁,采用线程同步队列来将未获取锁的线程进行排队,对于获取锁的操作,提供了公平和非公平两种方式。此外,它还提供了带超时机制的获取锁的方式(tryLock带有超时时间),以及非阻塞地方式尝试获取锁(无参数tryLock),支持了更多获取锁的场景。
而其条件等待与条件通知的机制是基于Condition来实现的。本文中将重点介绍一下Condition与ReentrantLock的关系,以及它是如何实现条件等待与条件通知机制的。
Condition
Condition其实是一个接口,其比较重要的方法为await系列方法(无超时时间,有超时时间...)和signal系列方法(signal单个线程和signal全部线程)。
正如Object的wait/notify机制是围绕着Object对象那样,ReentrantLock通过newCondition方法创建ConditionObject,这是AQS(AbstractQueuedSynchronizer)中的一个内置类。
下边我们来依次介绍一下await方法和signal方法。这是两个主干方法,基于类似的思路可以实现带超时时间的await方法,通知全部条件等待线程的signalAll方法。
await方法
await方法执行流程如下,大部分操作上我都进行了标注。
public final void await() throws InterruptedException {
// 如果线程已被中断,则抛中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 将本线程包装成节点,加入到条件等待队列的队尾
// 在该方法中,先判断队尾节点是否存在(即队列中已有等待节点),如果节点已经存在
// 但节点不是Node.CONDITION状态,则说明该节点对应的线程已经被CANCEL,去除这些
// CANCEL掉的节点,从后往前找到Node.CONDITION状态的节点,排在它后边,如果没有
// 这样的节点,那么本节点就是条件等待队列中的头节点
Node node = addConditionWaiter();
// 释放锁,由于await方法执行的时候一定是持有锁,所以其释放掉锁之后,线程同步队列
// 中的其他线程就可以尝试获取锁了
int savedState = fullyRelease(node);
int interruptMode = 0;
// 这里我们稍后分析一下
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 参考ReentrantLock.lock方法,执行到这里说明该线程已经被唤醒了,
// 此时它将可以去尝试获取锁了,获取到就可以继续向下执行,反之则park
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
下边来看一下while循环。Node的prev和next是指其在线程同步队列上的前置节点与后继节点;Node在条件等待队列的后置节点为nextWaiter。所以检查Node的prev和next就能知道节点是否已经在同步队列上了。
final boolean isOnSyncQueue(Node node) {
// 如果waitStatus为Node.CONDITION,那么一定不在线程同步队列中
// 如果节点没有前置节点了,那么已经被唤醒了
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);
}
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 cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 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);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
首先获取条件等待队列中的第一个节点,通过CAS操作将其waitStatus变成0,然后加入到线程同步队列中,检查其前置节点的状态(因为前置节点的状态对当前节点后续行为影响很大)。如果其前置节点已经被CANCEL了,或者设置SIGNAL状态失败,则说明轮到当前节点对应的线程获取锁了,将该线程唤醒。
实际使用场景
锁与条件等待/唤醒机制在实际工程应用中使用得非常频繁,所以涉及到多线程并发、线程挂起/恢复的场景通常都会对此有所涉及。在这里,我们举一个非常简单的例子加以说明。
关注JDK的ArrayBlockingQueue的实现,在这里我们只选取了能体现ReentrantLock和Condition的代码片段。我们关注put/take方法,这两个方法采用阻塞的方式从blockingQueue中添加/获取元素,所谓阻塞方式是指,当队列满时,put操作将被阻塞;当队列空时,take操作将被阻塞。那么阻塞操作是如何实现的呢?对队列(临界资源)的线程安全性又是如何实现的呢?让我们来看一下代码。
首先ArrayBlockingQueue中带有lock,每次进行put/take操作的时候,会先尝试获取锁;操作完毕后会释放锁。
另外,关注到两个Condition,分别是notEmpty和notFull,其在ArrayBlockingQueue的构造函数中,被赋值为lock.newCondition,也就是说这两个Condition都是基于lock的。
当put的时候,首先尝试获取锁,然后判断是否队列已满,满的话notFull.await,将线程挂起,等待队列不满的时候唤醒。
当take的时候,同样尝试获取锁,然后判断队列是否已空,空的话notEmpty.wait,将线程挂起,等待队列中有元素的时候唤醒。
初始状态下,队列为空,take操作被阻塞,当put操作进行时,会在将元素入队之后,调用notEmpty.signal,唤醒被挂起的take线程,使得挂起的take操作的线程开始工作;假设生产者速度远远大于消费者速度,当队列中元素已满时,put操作的线程被挂起,等待take操作通过消费队列中的数据,调用notFull.signal,唤醒挂起的执行put操作的线程。这两个锁相互作用,完成阻塞队列的功能。
/** 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 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();
}
}
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;
count++;
notEmpty.signal();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}