发生中断请求的条件是_Java显式锁的中断、条件队列的排队与唤醒源码分析

引言

上一节介绍了 ReentrantLock 类的 lock 锁获取流程,本节继续来说说它的其他两个知识点:

  • lock 和 lockInterruptibly 的区别
  • newCondition() ,条件队列的实现逻辑

lock 和 lockInterruptibly 的区别

ReentrantLock 的 lock 方法有几种获取锁方式:

  1. tryLock(),tryLock(long ,TimeUnit) ,可轮询的、可定时地获取锁;
  2. lock() ,无条件地轮询获取锁,锁等待期间,线程可被中断;
  3. lockInterruptibly() ,可中断的锁获取方式,锁等待期间,线程可被中断。

lock() 和 lockInterruptibly() ,这两个方法都能响应中断请求,但是区别在哪里呢?

分析源码,笔者发现 lock 方法默认处理了中断请求,一旦监测到中断状态,则中断当前线程;而 lockInterruptibly() 则直接抛出中断异常,由上层调用者区去处理中断,一起来看看源码细节。

lock 源码

lock 方法在获取锁的过程中,忽略了中断,并在成功获取锁之后,再根据中断标识处理中断,即 selfInterrupt 中断自己。 再回忆下 acquire 的源码:

/**  *默认处理中断方式是selfInterrupt */public final void acquire(int arg) {    if (!tryAcquire(arg) &&        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        selfInterrupt();}

acquireQueued,在 for 循环中无条件重试获取锁,直到成功,同时返回线程中断状态。for 循正常返回时,必定是成功获取到了锁,它的源码是这样:

/** *无条件重试,直到成功返回,并且记录中断状态 */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; // help GC                failed = false;                return interrupted;            }            if (shouldParkAfterFailedAcquire(p, node) &&                parkAndCheckInterrupt())                interrupted = true;//记录中断状态        }    } finally {        if (failed)            cancelAcquire(node);    }}

lockInterruptibly 源码

可中断加锁,在锁获取过程中不处理中断状态,而是直接抛出中断异常,由上层调用者处理中断。具体源码如下:

private void doAcquireInterruptibly(int arg)    throws InterruptedException {    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; // help GC                failed = false;                return;            }            if (shouldParkAfterFailedAcquire(p, node) &&                parkAndCheckInterrupt())                throw new InterruptedException();//抛出中断异常        }    } finally {        if (failed)            cancelAcquire(node);    }}

源码差别在 parkAndCheckInterrupt() 后面的那行:

  • acquireQueued 是 interrupted = true,记录中断状态;
  • doAcquireInterruptibly 是 throw new InterruptedException() ,抛出中断异常。

由于抛出了中断异常,所以 lockInterruptibly 方法的返回途径有两种,一种是 for 循环结束,正常获取到锁;另一种是线程被唤醒后检测到中断请求,则立即抛出中断异常,方法结束,同时该线程放弃获取锁请求。

差异分析

ReentrantLock 的中断和非中断加锁模式的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。

lock 方法会忽略中断请求,继续获取锁直到成功;而 lockInterruptibly 则立即响应中断,并抛出中断异常,由上层调用者处理中断。

那么,为什么要分为这两种模式呢?这两种加锁方式分别适用于什么场合呢?

根据它们的实现语义来理解,笔者认为 lock 适用于锁获取操作不受中断影响的情况此时可以忽略中断请求正常执行加锁操作。因为该操作仅仅记录了中断状态,执行 Thread.currentThread().interrupt()操作后,只是恢复了中断状态为 true,并没有对中断进行响应。

如果要求被中断线程不能参与锁的竞争,则应该使用 lockInterruptibly 方法,因为它一旦检测到中断请求,会立即返回,并且取消锁获取操作,不再参与锁的竞争。即 finally中 cancelAcquire 的操作。

newCondition 创建条件队列

AQS.ObjectCondition 是显式锁条件队列的抽象类,ReentrantLock 的 newCondition() 方法返回的是一个条件队列的实例。

什么是条件队列

第二节的锁部分也有提及过条件队列,这里再啰嗦一下。条件队列使得一组线程能够通过某种方式来等待特定的条件变成真,条件队列的元素是一个个正在等待某种状态的线程,这个等待过程其实就是阻塞线程。

Java 的内置锁【synchronized 语义对应的同步机制】,关联着一个内置条件队列。Object 的wait/notify/notifyAll 等方法构成了内置条件队列的 API,它们自动将内置锁与内置条件队列关联起来。 内置条件队列需要内置锁保护,即:调用对象 X 的 wait /notify 等方法,必须持有对象 X 的锁。这是因为状态处于并发环境下,“等待依赖状态的某个条件”与“维护状态的一致性”是绑定在一起的。

条件队列提供了一种挂起方式,当某个线程等待的条件非真时,挂起自己并释放锁,一旦等待条件为真,则立即醒来。这就是条件队列提供的主要功能,阻塞和唤醒

显式条件队列

内置锁的局限在于每个内置锁只能关联一个条件队列,无法满足等待多个条件等待场景。与内置锁对应的是显式锁,显式锁关联的条件队列是显式条件队列,它可以与多个条件队列关联。

Condition 是显式条件队列的顶层抽象接口,它是 Object 的 wait/notify/notifyAll 等方法的扩展,提供了在一个锁对象上设置多个等待条件的功能,基本方法是 await 和 signal ,代表着阻塞和唤醒。

显式条件队列必须和显式锁一起使用,因为对共享状态变量的访问发生在多线程环境下,原理与内部条件队列一样,因此 Condition 一般是作为 Lock 的内部类实现的。

AQS 的 ConditionObject 是用双向链表来实现队列结构的,类图结构为:

c35fbfc3c74f121a30067967a7816d65.png

ConditionObject 记录了头尾两个 Node 节点,提供了阻塞、唤醒等方法,基本与内置锁的 API 一致:

  • 阻塞操作,会调用 addConditionWaiter ,将某个线程加入等待队列;
  • 唤醒操作,会调用 doSignal ,将某个线程节点移除等待队列。

条件队列的节点状态

阻塞操作,向队列添加一个等待线程时,它会设置节点的 waitingStatus 为 Condition,标识当前节点正处于条件队列中。为了理解阻塞和唤醒的流程,我们来看看节点状态转换图:

7a62d409d0b98c6c5466338c13677e73.png


结合前面的源码,总结下 Node 的各个状态的作用:

1、Cancelled ,取消状态,主要是解决线程在持有锁时被外部中断的逻辑,lockInterrutibly 是基于该状态实现的。

2、Condition ,阻塞状态,需要等待某种条件时的状态。

3、Signal 唤醒状态,是 AQS 等待队列阻塞后继节点的标识,一个等待获取锁的线程,只有在其前驱节点等待 SIGNAL 时才会被阻塞,否则一直执行自旋尝试,以减少线程调度的开销。

等待和唤醒操作

条件队列上的阻塞和唤醒,本质是线程节点在 AQS 线程等待队列和条件队列之间相互转移的过程。

当线程需要等待某个条件时,它会被加入到条件队列,并释放锁;当某个线程在条件队列中被唤醒时,它会从条件队列中转移到 AQS 等待队列。一个 Condition 实例代表着一个条件队列,我们可以通过 Lock 的 newCondition 来创建多个条件队列。

为了理解节点在两个队列直接的转移过程,先看看阻塞和唤醒操作流程:

689f09c60201bd908009ec915d94ce45.png


核心是两个队列,AQS 的等待队列和 Condition 的条件队列,线程满足锁竞争条件,则进入 AQS 的等待队列;线程需要等待某个条件发生,则进入Condition 的条件队列。

条件队列引发的思考

显式条件队列弥补了内置条件队列只能关联一个条件的缺陷,同时继承了 Lock 对象的公平性。在Condition 类定义中,与 Object 的 wait/notify/notifyAll 等价的方法是 await/signal/signallAll,同时它也继承有 Object 的这三个方法,所以使用的时候需要注意调用的方法,不能混了。

使用显式锁时,必须手动释放锁,否则如果是独占锁,锁使用完成还被占者,其他线程就无法获取到该锁,标准的显式锁调用代码如下:

lock.lock();try{    // TODO }finally{    lock.unlock();}

此外,AQS 的两个队列都是链表队列,类的方法代码都相当简洁,尤其是节点移除队列操作过程中,都及时释放了所占内存。

这是我见到过的第三处及时释放 GC 的的代码了,最初是 ArrayList 的元素 remove 方法看到的,然后是 HashMap 的动态扩容数组转移操作置空无效元素,接着是最近看 AQS 的元素唤醒和锁释放操作。关注GC的确是最近开始形成的一种编程意识。

除了更深刻地理解 API ,源码中随处可见的严谨性细节,会对自己产生一些冲击。相似的处理方式,看到的次数多了,会记住并试着模仿应用,这就是笔者阅读源码额外收获……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值