并发之 Java中的锁
本节内容总结自《Java 并发编程的艺术》
5.5.1 Lock接口
Lock接口的作用
- 它提供了与synchronized关键字类似的同步功能,只是在使用时需要显示的获取和释放锁。虽然他缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized不具备的同步特性。
- 一般在finally块中释放锁,目的是保证锁在获取之后,最终能被释放。一般不要将获取锁写在try块中,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故释放。
Lock接口提供的synchronized关键字不具备的主要特性
- 尝试非阻塞的获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
- 能被中断的获取锁:与synchronized不一样,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。
- 超时获取锁:在指定的截止时间之前获取锁,如果未成功则返回。
Lock接口中的基本API
- lock():获取锁,调用该方法的线程将会获取锁,锁获取之后从该方法返回。
- lockInterruptibly():可中断的获取锁,该方法会响应中断,即在锁的获取过程中可以中断当前线程。
- tryLock():尝试非阻塞的获取锁,调用该方法后立刻返回,成功获取返回true,否则返回false。
- tryLock(long time):超时的获取锁,在下面三种情况下可以返回。①:当前线程在超时时间内获取到了锁。②:当前线程在超时时间内被中断。③:超时时间结束,返回false。
- unlock():释放锁。
- Condition newCondition():获取等待通知组件Condition,该组件和当前的锁绑定,当前线程只有获取到了锁,才能调用该组件中的wait()方法,而调用之后,当前线程将释放锁。
5.5.2 队列同步器(AQS)
简述
- AQS是用来构建锁或者其他同步组件的基础框架,他使用了一个int成员变量表示同步状态通过内置的FIFO队列来完成资源获取线程的排队工作。AQS组要的使用方式是继承,子类通过继承同步器并实现他的抽象方法来管理同步状态。在抽象方法中对同步状态的更改是通过3个方法(getState(),setState(),compareAndSetState(int expect, int update))来进行操作的,他们能保证状态的改变是安全的。子类被推荐定义为自定义同步组件的静态内部类。同步器既支持独占式获取同步状态也支持共享式获取同步状态。
- AQS是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层操作。锁是面向使用者的。
队列同步器的接口
- 修改同步状态的三个方法
- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
- 同步器可重写的方法
- tryAcquire(int arg):独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
- tryRelease(int arg):独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
- tryAcquireShared(int arg):共享式获取同步状态,返回大于等于0的值表示获取成功,反之,获取失败。
- tryReleaseShared(int arg):共享式释放同步状态。
- isHeldExclusively():当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程独占。
- 同步器提供的模板方法
- acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法会调用重写的tryAcquire(int arg)方法。
- acquireInterruptibly(int arg):该方法可以响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出中断异常并返回。
- tryAcquireNanos(int arg, long nanos):在上一个方法的基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将返回false,否则返回true。
- acquireShared(int arg):共享式的获取同步状态,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态。
- acquireSharedInterruptibly(int arg, long nanos):在上一个方法的基础上可以响应中断。
- tryAcquireSharedNanos(int arg, long nanos):再上一个方法的基础上增加了超时限制。
- release(int arg):独占式的释放同步状态,该方法会在释放同步状态之后将同步队列中第一个节点包含的线程唤醒。
- releaseShared(int arg):共享式的释放同步状态。
- getQueuedThreads():获取等待在同步队列山的线程集合。
队列同步器的实现分析
- 同步队列
- 节点的属性类型与名称以及描述
- int waitStatus:等待状态。
- CANCELLED,值为1,由于在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,节点进入该状态将不会变化。
- SIGNAL,值为-1,当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行。
- CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用signal()之后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
- PROPAGATE:值为-3,表示下一次共享式同步状态获取将会被无条件的传播下去。
- INITIAL:值为0,初始状态。
- Node prev:前驱节点,当前节点加入同步队列时被设置(当前节点被添加到尾部)。
- Node next:后继节点。
- Node nextWaiter:等待队列中的后继节点。
- Thread thread:获取同步状态的线程。
- int waitStatus:等待状态。
- 同步器依赖一个双向队列完成同步状态管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点并将其插入同步队列的尾部,同时会阻塞当前线程,当同步状态释放时,会把同步队列中首节点的线程唤醒,使其再次尝试获取同步状态,后继节点在获取到同步状态时,又会将自己设置为首节点(这是通过获取同步状态成功的线程完成的)
- 当一个线程无法获取同步状态而加入同步队列的尾部时,需要通过CAS来保证多线程情况下对同步队列尾部插入的原子性。
- 节点的属性类型与名称以及描述
- 独占式同步状态获取与释放
获取
public final void acquire(int arg) {
if(!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代码主要完成了同步状态获取,节点构造,加入同步队列以及在同步队列中自旋等待的相关工作。主要逻辑是:首先调用自定义同步器实现的tryAcquire()方法,该方法保证线程安全的获取同步状态。如果同步状态获取失败,则构造同步节点(Node.EXCLUSIVE表示独占式)并通过addWaiter(Node node)方法将该节点加入同步队列尾部 ,最后调用acquireQueued(Node node, int arg)方法,使得节点以死循环的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞的线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在尾部添加
Node pred = tail;
if(pred != null) {
node.prev = pred;
if(compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 尾部没有节点则新建节点
enq(node);
return node;
}
private Node enq(final Node node) {
for(;;) { // 同步器通过死循环来保证节点的正确添加,只有通过CAS将节点设置为为节点之后,当前线程才会从该方法返回,否则当前线程不断尝试设置尾节点
Node t = tail;
if(t == null) {
if(compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if(compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
节点进入同步队列之后就会进入一个自旋的过程。每个节点都在自省的观察,当条件满足,获取到同步状态,就可以从这个自旋过程中退出。只有当前前驱节点是头节点才能够尝试获取同步状态。原因有两个,如下:①:头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是不是头节点。②:维护同步队列的FIFO原则。
同步队列中的节点基本不互相通信,而是简单的判断自己的前驱是否为头节点,就可以完成基本的自旋工作。对于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是不是头节点,如果是则尝试获取同步状态。前驱节点是头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for(;;) {
final Node p = node.predecessor();
if(p == head && tryAcquire(arg)) { // 如果前驱节点是头节点且可以获取到锁
setHead(node); // 设置当前节点为头节点
p.next = null; // heap GC
failed = false;
return interrupted;
}
if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if(failed) {
cancelAcquire(node);
}
}
}
释放
同步器的release()方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(继而使后继节点重新尝试获取同步状态。)unpardSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。
public final boolean release(int arg) {
if(tryRelease(arg)) {
Node h = head;
if(h != null && h.waitStatus != 0) {
unparkSuccessor(h);
}
return true;
}
return false;
}
关于独占式同步状态获取与释放的总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中等待自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease()方法释放同步状态,然后唤醒头结点的后继节点。
- 共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到这个状态。
共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞,独占式访问资源时,同一时刻其他访问均被阻塞。
在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg) 方法返回值大于等于0.如果当前节点的前驱节点为头结点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并且从自旋过程中退出。
public final void acquireShared(int arg) {
if(tryAcquireShared(arg) < 0) // 尝试获取共享锁
doAcquireShared(arg); // 进入自旋
}
public void doAcquireShared(int arg) {
final Node node = addwaiter(Node.SHARED); // 创建共享式同步队列中的节点
boolean failed = true;
try {
boolean interrupted = false;
for(;;) {
final Node p = node.predecessor();
if(p == head) { //如果前驱节点是头节点,则尝试获取共享锁
int r = tryAcquireShared(arg);
if(r >= 0) { // 如果获取成功
setHeadAndProgate(node, r); //则将该节点设置为同步队列的头节点并且设置waitStatus 为 PROPAGATE
p.next = null; // help GC
if(interrupted) // 自我中断
selfInterrupt();
failed = false;
return;
}
}
if(shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if(failed)
cancelAcquire(node);
}
}
释放
通过releaseShared(int arg)来释放同步状态,这个操作将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件,他和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态的线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态会同时来自多个线程。
public final boolean releaseShared(int arg) {
if(tryReleaseShard(arg)) {
doReleaseShared();
return true;
}
retur false;
}
- 独占式超时获取同步状态
首先介绍一下响应中断的同步状态获取过程:同步器提供了acquireInterruptibly(int arg)方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。超时获取同步状态过程可以被视作响应中断获取同步状态过程的增强版,doAcquireNanos(int arg, long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。
该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回。如果当前线程获取同步获取同步状态失败,则判断是否超时,没有超时则重新计算超时时间间隔nanosTimeout,然后使用当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker, long nanos) 方法返回)
超时等待时间非常的短的情况下,将不会使该线程进入超时等待状态,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时候再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。
acquire与超市获取同步状态在流程上非常相似,其主要区别在于未获取到同步状态时的处理。acquire在未获取到同步状态时会使当前线程一直处于等待状态,而doAcquireNanos(int arg, long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException{
long lastTime = System.nanoTime(); // 睡眠之前的时间
final Node node = addWaiter(Node.EXCLUSIVE); // 创建节点
boolean failed = true;
try {
for(;;) {
final Node p = node.predecessor();
if(p == head && tryAcquire(arg)) {
setHead(node); // 如果前驱节点为头节点,则获取同步状态,并设置为头节点
p.next = null; // heap GC
failed = false;
return true;
}
if(nanosTimeout <= 0) // 如果已经超时,则直接返回
return false;
if(shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanotime(); // 获取当前时间
nanosTimeout -= now - lastTime; // 当前时间减去睡眠之前的时间得到已经睡眠的时间,然后被原有超时时间减去,得到还应该睡眠的时间
lastTime = now;
if(Thread.interrupted)
throw new InterruptedException();
}
} finally {
if(failed)
cancelAcquire(node);
}
}
5.5.3 重入锁
定义
重入锁:顾名思义就是支持重进入的锁,他表示该锁能够支持一个线程对资源的重复加锁。
实现重进入
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。
- 锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
ReentrantLock中非公平锁实现为例
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if(c == 0) { // 如果当前所属于未被持有的状态
if(compareAndSetState(0, acquires)) { //尝试使用CAS获取锁
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;
}
ReentrantLock中释放同步状态时减少同步状态值
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if(Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if(c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free; // 只有锁完全释放时才返回true
}
公平锁与非公平锁的区别
- 获取锁的方式
- 公平锁:如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
- 非公平锁:只要CAS设置同步状态成功,则表示当前线程获取了锁。
- 性能
- 公平锁:公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换,一定程度上解决了饥饿问题。
- 非公平锁:虽然可能造成线程饥饿,但极少的切换线程,保证了其更大的吞吐量。
5.5.4 读写锁
定义
读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁。Java提供了ReentrantReadWriteLock。
ReentrantReadWriteLock的特性
- 公平性选择:支持非公平性锁和公平的锁获取方式,吞吐量还是非公平优于公平。
- 重进入:该所支持重进入。以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后还能够再次获取写锁,同时也可以获取读锁。
- 锁降级:遵循获取写锁,获取读锁在释放写锁的次序,写锁能够降级为读锁。
读写锁的接口
ReentrantReadWriteLock中的方法
int getReadLockCount():返回当前读锁被获取的次数。该次数不等于获取读锁的线程数。
ing getReadHoldCount():返回当前线程获取读锁的次数。使用ThreadLocal实现。
boolean isWriteLocked():判断写锁是否被获取。
int getWriteHoldCount():返回当前写锁被获取的次数。
读写锁的实现分析
- 读写状态的设计:读写锁将变量切分成两个部分,高16位表示读,低16位表示写。可以通过state & 0xffff来获取低六位,通过state >>> 16 获取高六位。通过state + 1实现写锁的自增,通过state + (1 << 16) 来实现读锁的自增。
- 写锁的获取与释放:写锁是一个可重入的排他锁。如果当前线程已经获取了写锁,则增加写状态。否则,如果该线程不是获取写锁的线程,则当前线程进入等待状态。
- 如果读锁存在,那么写锁不能被获取。原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁获取,那么正在运行的其他线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,其他读写线程的后续访问均被阻塞。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if(c != 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(writeShouldBlock() || !compareAndSetState(c, c + acquires)) {
return false; // 如果写被阻塞或者通过CAS更新state失败
}
setExclusiveOwnerThread(current);
return true;
}
- 读锁的获取与释放
- 在没有其他写线程访问时,读锁总会被成功的获取。如果当前线程在获取读锁时,写锁已经被其他线程获取,则进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程增加读状态。
- 锁降级
- 锁降级是指把持住写锁,再获取到读锁,随后释放写锁的过程。
锁降级的应用:多个线程并发的进行数据处理时,为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。读锁的存在屏蔽了其他线程的写锁的获取,间接的实现了线程之间对数据的可见性。
5.5.5 LockSupport工具
LockSupport工具的作用
- LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能。以park开头的方法用来阻塞当前线程,以unpark(Thread thread)方法来唤醒一个被阻塞的线程。
LockSupport中的方法
- park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,才能从park()方法返回。
- parkNanos(long nanos):在上述方法的基础上增加了超时返回。
- parkUntil(long deadline):阻塞当前线程,直到deadline时间(从1970年开始到deadline时间的毫秒数)。
- unpark():唤醒处于阻塞状态的线程Thread。
- JDK 6中,LockSupport增加了park(Object blocker),parkNanos(Object blocker, long nanos)和parkUntil(Object blocker, long deadline)三个方法,用于实现阻塞当前线程的功能,其中参数blocker用来表示当前线程在等待的对象,该对象主要用于问题排查和系统监控。有阻塞对象的方法可以传递给开发人员更多的现场信息,方便问题定位。
5.5.6 Condition接口
Condition接口的作用
任何一个Java对象,都拥有一组监视器方法,这些方法与synchronized同步关键字配合可以实现等待 / 通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待 / 通知模式。Condition接口中实现了很多Object所提供的监视器中没有的方法。如下。
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock()获取锁 调用Lock.newCondition()获取Condition对象 |
调用方式 | 直接调用 如:object.wait() |
直接调用 如:condition.await(); |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在等待着状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition接口
- Condition定义了等待 / 通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是必须通过Lock对象的newCondition()方法获取。一般会将Condition对象作为成员变量。当调用await()方法之后,当前线程会释放锁并在此等待,而其它线程调用Conditin对象的signal()方法,通知当前线程后,当前线程才会从await()方法返回,并且在返回前已经获取了锁。
- Condition的部分方法以及描述
- await():当前线程进入等待状态直到被通知或中断,当前线程将进入运行状态且从await()方法返回的情况。①:其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒。②:其他线程中断当前线程。③:如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁了。
- awaitUninterruptibly():当前线程进入等待状态直到被通知,该方法对终端不敏感。
- awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时。返回值表示剩余的时间。如果在指定时间之前被唤醒,返回值就是剩余时间。如果返回值为负数或者0,就可以认定已经超时了。
- awaitUntil(Date deadline):当前线程进入等待状态直到被通知,中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,方法返回false。
- signal():唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁。
- sinallAll():唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。
Condition的实现分析
等待队列:等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上的等待的线程,如果一个线程调用了Condition.await()方法,那么该线程就会释放锁,构造成节点加入等待队列的尾部并进入等待状态。事实上,节点定义复用了同步队列中节点的定义。节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。
并发包中的Lock拥有一个同步队列和多个等待队列。同步器的每一个Condition对象都对应于一个等待队列。
等待
调用Condition的await()方法,会使当前线程进入等待队列并释放锁,同时线程进入等待状态。当从await()方法返回时,当前线程一定已经获取了Condition相关联的锁。
如果从同步器队列的角度看await()方法,当调用await()方法时,相当于同步队列的首节点移动到Condition的等待队列尾部。
该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。
public final void await() throws InterruptedException {
if(Thread.interrupted()) // 如果是通过中断唤醒等待线程,则抛出异常
throw new InterruptedException();
Node node = addConditionWaiter(); // 通过该方法把当前线程构造成一个新的节点并加入等待队列中
}
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点,在唤醒结点之前,会将节点移到同步队列中。调用signal()方法的条件是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
调用同步器的enq(Node node)方法,等待队列中的头节点线程安全的移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。被唤醒的线程将会从await()方法中的while循环中退出,进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
成功获取同步状态之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中的全部节点都移动到同步队列中,并依次唤醒每个结点的进程。
public final void signal() {
if(!isHeldExclusively()) // 当前线程必须持有锁
throw new IllegalMonitorStateException();
Node first = firstWaiter; // 获取等待队列中的第一个线程并将其唤醒
if(first != null)
doSignal(first);
}