head first java代码_Java显式锁ReentrantLock如何实现公平与排队

引言

AQS ,全称「 AbstratcQueuedSynchronizer 」,它是 Java 显式锁实现的基础框架,本质是一种队列结构,以先进先出的方式维护线程的阻塞和唤醒。JDK 源码中,AbstratcQueuedSynchronizer 类定义的注释是这样写的:

Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues. This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic {@code int} value to represent state.

简单翻译一下,就是开题的内容:

  1. 一个阻塞锁框架
  2. 依赖先进先出的等待队列

本章节,一起来跟踪一下 ReentrantLock 的源码,它是基于 AQS 实现的可重入的互斥锁。如果把这个类的源码吃透了,我们就可以按照 AQS 注释上的示例自定义同步锁了。

整体结构

ReentrantLock 类总数 700 多行,特别赞的是,代码很优雅,每个方法都很简洁。
跟踪源码,笔者绘制出的类结构是这样的:
​​​​​

ee073a397cef15a8778f0ef31c8337c3.png


首先,看看 ReentrantLock 类,它包含一个同步器成员变量 sync ,三个方法lock 、 unlock 和 newCondition。需要注意的是, lock 方法提供了几种获取锁方式:

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

其次,关注sync 这个成员变量,它是一个 AQS 抽象类的实例,在 ReentrantLock 中有两种实现子类 FairSync 和 NonfairSync,区别在于 lock 方法请求锁是否允许插队。

公平锁和非公平锁的差异

公平锁加锁时,不允许插队,直接执行 acquire 方法,源码为:

final void lock() {     acquire(1);} 

非公平锁在请求获取锁时,会先尝试 CAS 操作获取锁,尝试失败才进行排队。

 final void lock() {        if (compareAndSetState(0, 1))          setExclusiveOwnerThread(Thread.currentThread());        else          acquire(1);}

在竞争激烈的应用场景中,非公平锁的性能要高于公平锁,因为:活动线程直接尝试获取锁的时间可能比恢复一个阻塞线程,并把锁分配给它的时间短的多。线程的唤醒需要额外时间,从唤起到线程真正运行之间存在着严重的时延。这也是前面提到的插队效益,这里不再赘述了!

非公平锁 NonfairSync

非公平锁的 lock 方法 ,过程很简单,先插队请求锁,失败后再走正规的 acquire 途径获取锁:

02ffe80ab4f035e36d167a5f74d8c4c0.png


后面的 acquire 操作就是公平锁的执行流程了。

acquire,锁获取流程

公平锁和非公平锁最后都是走 acquire(1) 方法来获取锁的,它的源码,只有区区两行:

public final void acquire(int arg) {        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }

言简意赅:先尝试获取锁,如果失败,则将当前节点从队尾加入 AQS 队列等待。如果线程在等待过程时中断标识为真,则中断当前线程。

4e5585f13778b4fdcf29d0232da3c68f.png

tryAcquire(1)

tryAcquire(1) 是 acquire(1) 方法的第一个操作,它返回一个布尔值,标识是否成功获取到锁。跟踪源码,绘制出它的流程图:

51825de83fbf8749c682aaaca9a545f1.png


流程图简述:

  1. 检查锁是否被占用,如果未被占用,则执行 CAS 并返回 CAS 的结果;如果锁被占用,则继续;
  2. 检查当前线程持是否是锁的持有者,如果是,说明是线程重入,将锁的重入次数累加一,返回 true;
  3. 否则,获取操作失败,返回 false 。

addWaiter 入队流程

锁获取失败时,需要将当前线程加入条件队列排队,这就是 addWaiter 方法的工作。addWaiter 将当前线程封装成一个队列节点 Node 的实例,并以 for 循环的方式重复尝试将节点插入 AQS 队列,直到操作成功返回该 Node。

AQS 维持了一个链表,具有 head 和 tail 两个属性,初始时均为空。在添加第一个节点时,先创建一个虚拟的头节点【即 new Node() 没有任何信息的节点】,并将 tail 指向 head。新节点从队尾以 CAS 原子操作插入,插入操作在 for 循环中,能保证线程一定会被添加到等待队列里。

完整的流程图是这样的:

4bad8ba4856dbb1602ff3e358f638a15.png


核心是,为当前线程创建一个等待节点,并成功加入等待队列。

acquireQueued 排队线程获取锁流程

acquireQueued 是 acquire 第三个步骤,它封装了排队线程获取锁的过程,绘制流程图如下:

f71bb16cf501fb1a8fa542394542304c.png

获取锁失败的线程,被 addWaiter 方法加入等待队列后,会继续执行acquireQueued 方法,不断重试,直到线程被阻塞或者成功拿到锁为止。它的返回值是线程中断标识,即如果在等待锁过程中,该线程被中断,返回 true 给 acquire ,由 acquire 方法处理中断请求。

为什么会有这种判断呢?笔者的理解是:为了保证线程因等待锁而被阻塞的过程里,外部传递的中断信号不会被淹没。就是说,如果在该线程被 park 阻塞后,其他线程向它发送了中断信号,它是没办法响应该中断请求的。

所以 parkAndCheckInterrupt 操作就很有必要了,它在线程唤起时检查中断标志并通知该线程,由该线程自己去响应中断中断信号,对应 acquire 的 selefIntrupt() 做的事情。

shouldParkAfterFailedAcquire

某个线程尝试获取锁,如果失败了,会调用 shouldParkAfterFailedAcquire 方法,根据前驱节点的状态判断是否需要挂起该线程,它返回一个布尔值,标识当前节点是否需要被挂起。

具体流程如下:

17dc9cf87aecee96e2e199b42d7cfd9e.png


至此,锁获取操作流程分析结束。ReentrantLock 的 lock 方法执行的结果是,要么线程被挂起,要么循环轮询获取锁直到成功设置状态为1(占用状态),然后被移除排队队列。

某个等待线程只有在其前驱节点的等待状态为 SIGNAL【前驱在等待条件队列执行唤醒操作】时,才会被阻塞,其他情况下都处于循环重试的过程中。

笔者认为这样根据前驱状态阻塞线程或者自旋重试,而不是直接挂起线程的处理很精妙。因为前驱还处于阻塞、等待唤醒的状态,说明自己获取锁无望,也就没有尝试的必要了。这样可以避免线程调度的资源消耗,毕竟,线程的挂起和唤醒是需要付出代价的。

unlock 操作分析

unlock 操作用来释放锁,只有锁的持有者才能调用,否则会抛出IllegalMonitorStateException 异常。代码也比较简单:

    public void unlock() {        sync.release(1);    }

还是直接调用同步器 sync 的 release 方法:

    public final boolean release(int arg) {        if (tryRelease(arg)) {            Node h = head;            if (h != null && h.waitStatus != 0)                unparkSuccessor(h);            return true;        }        return false;    }

尝试释放锁,如果释放成功、且等待队列非空,则唤醒它的后继节点。
唤醒后继节点的条件是,后继节点非空且非取消状态:

  • 如果满足,则调用 LockSupport 的 unpark 唤醒;
  • 否则,一直循环,直到找到一个可唤起的后继节点为止。

启示录

ReentrantLock 是 Java 大师的手笔,功力可见一斑。看源码,领会一两点编码技巧,以后可以应用到自己的开发工作中。

最后,总结一下这个类的编码艺术:

  1. 依赖抽象的 sync ,它是面向抽象的编程手法
  2. 巧妙用了队列这种数据结构
  3. 等待锁的过程中,如果某个节点的前驱节点处于阻塞状态时,当前节点也不再做无谓的挣扎
  4. 合理拆分方法,简洁优雅

笔者还是 2015 年看的 ReentrantLock 源码,本文的所有流程图和类图,都是当时跟踪源码时所绘的,为了编写专栏,而对一篇旧文的重新整理。再看一遍自己当时想明白的一些事情,还是很有启发的。

这也是记录的意义呀,虽然跨越了时空,有些感悟还是相通的!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值