上次对于显示锁和队列同步器的学习已经提到,我们可以使用Java提供的队列同步器AQS实现自定义的同步组件。同样,Java自身也为我们提供了很多可用的锁(或同步组件),如重入锁ReentrantLock和读写锁ReentrantReadWriteLock等,它们也是通过AQS实现的,并且继承了Lock接口。
与内置锁synchronized不同,Lock接口提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方式都是显式的,这也是显式锁的由来。java.util.concurrent.locks
中的 Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock
的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
重入锁
重入锁ReentrantLock,顾名思义,是指可重入的锁,即当前拥有锁的线程能过对此资源反复进行加锁,而不会被阻塞。在Java中关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入(具体可参考这篇文章)。
除了可重入外,ReentrantLock还支持公平锁和非公平锁两种方式。
1、实现可重入
要想实现可重入性,就要解决以下两个问题:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功
- 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功
ReentrantLock是通过组合对自定义同步器来实现锁的获取和释放的,以非公平锁为例,重入获取锁时:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//获取同步状态值
//为0,说明该锁未被任何线程占有,该锁能被当前线程获取
if (c == 0) {
if (compareAndSetState(0, acquires)) {//CAS重置同步状态值
setExclusiveOwnerThread(current);
return true;
}
}
//否则,锁已被占有,检查占有线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 是就再次获取,计数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
成功获取锁的线程再次获取锁后,只是增加了同步状态值state,又由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功;所以ReentrantLock在每次释放同步状态时也必须减少同步状态值,锁释放如下代码:
protected final boolean tryRelease(int releases) {
//参数值releases、acquires一般都为1
int c = getState() - releases; //同步状态值减1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//只有当同步状态值为0时,锁成功被完全释放,返回true
free = true;
setExclusiveOwnerThread(null);//设置锁当前所属线程为空
}
//同步状态值不为0,锁未被完全释放,返回false
setState(c);
return free;
}
需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放tryRelease方法必须返回false,只有被释放n次才算成功释放,返回true。
2、公平与非公平
所谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。ReentrantLock的无参构造方法默认是构造非公平锁,构造公平锁则需要布尔参数值为true。我们可以这样使用:
Lock fairLock = new ReentrantLock(true);
Lock unfairLock = new ReentrantLock(false);
//也可以无参
Lock unfairLock = new ReentrantLock();
而ReentrantLock内部是通过将Sync继承AQS并设为抽象类,再用NonfairSync和FairSync两个静态内部类分别实现非公平和公平锁的语义。
//定义Sync抽象类继承AQS同步器
abstract static class Sync extends AbstractQueuedSynchronizer {....}
//非公平锁NonfairSync继承抽象类Sync,实现其语义
static final class NonfairSync extends Sync {....}
//公平锁FairSync继承抽象类Sync,实现其语义
static final class FairSync extends Sync {....}
//ReentrantLock默认构造为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//设置公平锁,构造参数为true
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
上文中已介绍非公平锁使用nonfairTryAcquire(int acquires)方法获取同步状态,对于非公平锁,只需要CAS设置同步状态成功,则表示当前线程获取到了锁,而公平锁则则多了一道防线,ReentrantLock的TryAcquire(int acquires)方法即为公平的获取同步状态。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
该方法与上文nonfairTryAcquire(int acquires)方法完全类似,唯一的不同只有判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,表示有线程比当前线程更早的请求获取锁,因此为了公平需要等待前驱线程获取并释放锁之后才能继续获取锁。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
如上所述,公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。似乎公平锁功能更好,那么我们为什么还要默认使用非公平锁呢?那是因为:
- 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
对于ReentrantLock公平锁和非公平锁的理解,这篇文章讲的很生动,值得一看。
3、Lock锁和synchronized锁的区别
读写锁
在AQS同步器的实现原理学习中我们知道了获取同步状态的两种方式:独占式和共享式。而ReentrantLock或者之前的synchronized都是独占式锁(或者说是排他锁),尽管ReentrantLock和synchronized都是可重入的(synchronized锁是隐式可重入)锁,但这些锁同一时间只允许一个线程对其进行访问,而共享式锁在同一时刻可允许多个线程访问。
读写锁的实现就依靠了共享锁的原理,其在读访问时,允许多个读线程访问;而在写访问时,只允许一个写线程,所有其他的读线程和写线程都被阻塞。
1、ReadWriteLock接口
读写锁维护了一对锁来实现其语义,分别是一个读锁(共享锁)和一个写锁(独占锁),通过分离读锁和写锁,使得其并发性相对于一般的排他锁有了很大的提升。
ReadWriteLock是专门为了实现读写锁而设计的接口,它只定义了获取读锁和获取写锁两个方法,而实际的功能由它的实现类ReentrantReadWriteLock来详细实现。
2、ReentrantReadWriteLock读写锁
ReentrantReadWriteLock是ReadWriteLock的实现类,除了接口定义的方法外,它还提供了一些便于外界监控其内部工作状态的方法,ReentrantReadWriteLock提供的特性如下:
- 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
- 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
- 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
ReentrantReadWriteLock主要由以下方法实现其功能:
ReentrantReadWriteLock.ReadLock
readLock()
返回用于读取操作的锁。ReentrantReadWriteLock.WriteLock
writeLock()
返回用于写入操作的锁。boolean
isWriteLocked()
判断写锁是否被获取。int
getWriteHoldCount()
查询当前线程在此锁上保持的重入写入锁数量。int
getReadLockCount()
返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,例如一个线程连续重入的获取读锁N次,那么占据读锁的线程数是1,而此方法返回Nint
getReadHoldCount()
返回当前线程获取读锁的次数,即当前线程重入锁的次数。
读写锁的使用:
我们知道HashMap是线程不安全的,所以下面使用读写锁实现一个HashMap为基础的线程安全的缓存的简单例子
public class Cache {
static Map<String,Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock read = rwl.readLock(); //创建读锁
static Lock write = rwl.writeLock(); //创建写锁
//get读取key键对应的value值,读锁
public static final Object get(String key) {
read.lock();
try {
return map.get(key);
}finally {
read.unlock();
}
}
//put写入key键对应的value值,返回旧value值,写锁
public static final Object put(String key,Object value) {
write.lock();
try {
return map.put(key,value);
}finally {
write.unlock();
}
}
}
在上面的代码示例中,同时使用读写锁的读锁和写锁来保证Cache类的线程安全,使用非常的简单,那具体是怎么实现的呢
3、读写锁的实现
1.读写状态的设计
对于AQS同步器的学习后我们知道,同步器管理一个int类型整数信息state,这个整数状态信息可以表示任何的状态,例如,重入锁ReentrantLock用来表示所有者线程已经重复获取该锁的次数;信号量Semaphore用来表示剩余的许可数量;FutureTask用来表示任务的运行状态(尚未运行、正在运行、已完成已经已取消)等。
读写锁的自定义同步器,需要维护多个读线程和一个写线程的状态,但是AQS只提供了一个整型变量,那他是如何做的呢?
读写锁采取了”按位分割“的方法来充分的利用一个整型变量state,将此32位的int变量state分割成为了两个部分,高16位表示读,低16位表示写,如图所示:
如图的当前同步状态表示一个线程已经获取到了写锁,并且又重入了2次,同时也获取到了读锁。
读写锁通过高效的位运算来迅速确定读和写各种的同步状态,写状态为state & 0x0000FFFF(将高16位全部抹去),读状态等于state >>> 16(无符号右移16位);所以当写状态增加1(获取写锁)时,等于state+1,读状态增加1(获取读锁)时,等于state+(1<<16),即state+0x00010000。
2、写锁的获取与释放
写锁的获取:
写锁是一个可重入的排他锁,这与ReentrantLock的语义是一样的。如果当前线程已经获取到了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(state高16位的读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,具体实现如下:
//重写AQS的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//写锁当前的同步状态
int c = getState();
//写锁被获取的次数,低16位,即state & 0x0000FFFF
int w = exclusiveCount(c);
if (c != 0) {
//当存在读锁(c!=0,w=0即高16位不为0)或者当前线程不是已经获取写锁的线程的话
if (w == 0 || current != getExclusiveOwnerThread())
return false; // 当前线程获取写锁失败
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//当前线程获取写锁,支持可重复加锁
setState(c + acquires);
return true;
}
//写锁未被任何线程获取,当前线程可获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
相较于一般的排他锁,该方法除了重入条件,还增加了读锁是否存在的判读;当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败。
写锁的释放:
写锁的释放与ReentrantLock基本类似,每次释放减少写状态,当写状态为0时表示写锁已被释放,从而等待的读线程和写线程继续竞争访问锁,同时前次写线程的修改对后续读写线程都可见。
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//同步状态减去写状态
int nextc = getState() - releases;
//当前写状态是否为0,为0则释放写锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
//不为0则更新同步状态
setState(nextc);
return free;
}
3、读锁的获取和释放
读锁是一个支持重入的共享锁,它能被多个线程同时获取,在没有其他写线程访问(state低16位的写状态为0)时,读锁总能够被成功的获取,但是会增加读状态(state的高16位加1),此增加操作是线程安全的,依靠的是CAS来保证的。
共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法,读锁的获取实现方法为:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//如果写锁已经被获取或者获取写锁的线程不是当前线程的话
//当前线程获取读锁失败并返回-1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);//获取state同步状态的高16位,采用state>>>16
if (!readerShouldBlock() &&
r < MAX_COUNT &&
//当前线程获取读锁
compareAndSetState(c, c + SHARED_UNIT)) {
//下面的代码主要是新增的一些功能,比如getReadHoldCount()方法
//返回当前获取读锁的获取次数
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//处理在上述CAS操作失败的自旋实现重入性
return fullTryAcquireShared(current);
}
读状态是所有线程获取读锁次数的总和,但不保存每个线程各自获取读锁的次数,这个次数由各自的线程保存在各自的ThreadLocal中,线程自身维护。
读锁状态的增加是依靠CAS实现的,如果当前线程获取了写锁或者写锁未被获取,则当前线程CAS增加读状态,成功获取读锁。
读锁的每次释放,也必须保证是线程安全的,可能多个线程同时释放读锁,同时减少读状态,减少值为(1<<16)。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 为了实现getReadHoldCount等新功能
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
// 读锁释放 将同步状态减去读状态即可
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
4、锁降级
锁降级指的是写锁降级为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程是不能称之为锁降级的。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先去拥有的)写锁的过程。
void processCachedData() {
rwl.readLock.lock();
if (!cacheValid) {
//必须先释放读锁
rwl.readLock().unlock();
//锁降级从写锁获取到开始
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
//释放写锁前必须先获取读锁
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); //把持读锁,释放写锁
}
}
try {
use(data);//使用数据
} finally {
rwl.readLock().unlock();
}
}
锁降级中读锁的获取是很有必要的,主要是为了保证数据的可见性。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取了写锁并修改了数据,那么当前线程将无法感知线程T的数据更新。如果当前线程获取读锁,遵循锁降级的流程,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
辅助工具类
1、LockSupport工具类
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。LockSupport提供了一组公共的静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,所以它也成为了构建同步组件的基本工具。
LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。
park()和unpark()不会有 “Thread.suspend和Thread.resume所可能引发的死锁” 问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。
如果调用线程被中断,则park方法会返回。同时park也拥有可以设置超时时间的版本。
需要特别注意的一点:park 方法还可以在其他任何时间“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里调用此方法。从这个意义上说,park 是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高效。
2、Condition辅助接口
任何一个Java对象,都有一组监视器方法wait、notify 和 notifyAll等,这些方法与synchronized配合使用可以实现等待通知模式。而Condition则是将Object的这些通信方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set),其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 通信方法的使用。
Condition的使用:
通过对比两者在使用上的差异,可以更好的理解其不同的特性:
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
获取一个Condition必须通过Lock的newCondition()方法。下面通过一个有界队列的示例来深入了解Condition的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现“空位”。
public class BoundedQueue<T> {
private Object[] items;
// 添加的下标,删除的下标和数组当前数量
private int addIndex,removeIndex,count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private 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;
if(++addIndex == items.length)
addIndex = 0;
++count;
notEmpty.signal();
}finally{
lock.unlock();
}
}
//由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
@SuppressWarnings("unchecked")
public T remove() throws InterruptedException{
lock.lock();
try{
while(count == 0)
notEmpty.await();
Object x = items[removeIndex];
if(++removeIndex == items.length)
removeIndex = 0;
--count;
notFull.signal();
return (T)x;
}finally{
lock.unlock();
}
}
}
首先需要获得锁,目的是确保数组修改的可见性和排他性。当数组数量等于数组长度时,表示数组已满,则调用notFull.await(),当前线程随之释放锁并进入等待状态。如果数组数量不等于数组长度,表示数组未满,则添加元素到数组中,同时通知等待在notEmpty上的线程,数组中已经有新元素可以获取。
在添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。回想之前提到的等待/通知的经典范式,二者是非常类似的。
Condition的实现原理:
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个等待队列,该队列是Condition对象实现等待/通知功能的关键。
下面将分析Condition的实现,主要包括:等待队列、等待和通知。
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态 。一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。
事实上,Condition节点的定义服用了AQS队列同步器中节点的定义,也就是说,同步队列和等待队列的节点类型是一样的,都是AQS的静态内部类AbstractQueuedSynchronizer.Node。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下图所示:
如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如下图所示:
等待方法
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
由图中可以看出,调用await()方法的线程是成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成新节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
需要注意的是,同步队列的首节点不会直接加入等待队列(虽然节点类型是一样的),而是通过addConditionWaiter()方法把当前线程构造成一个新的节点再将其加入等待队列中。
唤醒方法
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
调用signal方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中(自旋)。成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
参考文章:
《Java并发编程的艺术》
《Java并发编程实战》
《Java核心技术 卷1》