ReentrantLock 是 Java 在 JDK1.5 引入的显式锁, 在实现原理和功能上都和内置锁 (synchronized) 上都有区别, 在文章最后我们再比较这两个锁.
首先我们要知道 ReentrantLock 是基于 AQS 实现的, 所以我们得对 AQS 有所了解才能更好的去学习掌握 ReentrantLock , 关于 AQS 的介绍可以参考我之前写的一篇文章 《一文带你快速掌握 AQS》 , 这里简单回顾下 AQS .
AQS 回顾
AQS 即 AbstractQueuedSynchronizer 的缩写, 这个是个内部实现了两个队列的抽象类, 分别是 同步队列 和 条件队列 . 其中 同步队列 是一个双向链表, 里面储存的是处于等待状态的线程, 正在排队等待唤醒去获取锁, 而 条件队列 是一个单向链表, 里面储存的也是处于等待状态的线程, 只不过这些线程唤醒的结果是加入到了同步队列的队尾, AQS 所做的就是管理这两个队列里面线程之间的 等待状态 - 唤醒 的工作.
在同步队列中, 还存在 2 中模式, 分别是 独占模式 和 共享模式 , 这两种模式的区别就在于 AQS 在唤醒线程节点的时候是不是传递唤醒, 这两种模式分别对应 独占锁 和 共享锁 .
AQS 是一个抽象类, 所以不能直接实例化, 当我们需要实现一个自定义锁的时候可以去继承 AQS 然后重写 获取锁的方式 和 释放锁的方式 还有 管理 state , 而 ReentrantLock 就是通过重写了 AQS 的 tryAcquire 和 tryRelease 方法实现的 lock 和 unlock .
ReentrantLock 原理
通过前面的回顾, 是不是对 ReentrantLock 有了一定的了解了, ReentrantLock 通过重写 锁获取方式 和 锁释放方式 这两个方法实现了 公平锁 和 非公平锁 , 那么 ReentrantLock 是怎么重写的呢, 这也就是本节需要探讨的问题.
ReentrantLock 结构
首先 ReentrantLock 继承自父类 Lock , 然后有 3 个内部类, 其中 Sync 内部类继承自 AQS , 另外的两个内部类继承自 Sync , 这两个类分别是用来 公平锁和非公平锁 的.
通过 Sync 重写的方法 tryAcquire , tryRelease 可以知道, ReentrantLock 实现的是 AQS 的独占模式, 也就是独占锁, 这个锁是悲观锁 .
ReentrantLock 有个重要的成员变量:
private final Sync sync;
这个变量是用来指向 Sync 的子类的, 也就是 FairSync 或者 NonfairSync , 这个也就是多态的 父类引用指向子类 , 具体 Sycn 指向哪个子类, 看构造方法:
- public ReentrantLock() {
- sync = new NonfairSync();
- }
- public ReentrantLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
- }
ReentrantLock 有两个构造方法, 无参构造方法默认是创建 非公平锁 , 而传入 true 为参数的构造方法创建的是 公平锁 .
非公平锁的实现原理
当我们使用无参构造方法构造的时候即 ReentrantLock lock = new ReentrantLock() , 创建的就是非公平锁.
- public ReentrantLock() {
- sync = new NonfairSync();
- }
- // 或者传入 false 参数 创建的也是非公平锁
- public ReentrantLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
- }
lock 方法获取锁
lock 方法调用 CAS 方法设置 state 的值, 如果 state 等于期望值 0 (代表锁没有被占用), 那么就将 state 更新为 1 (代表该线程获取锁成功), 然后执行
setExclusiveOwnerThread
方法直接将该线程设置成锁的所有者. 如果 CAS 设置 state 的值失败, 即 state 不等于 0 , 代表锁正在被占领着, 则执行 acquire(1) , 即下面的步骤.
nonfairTryAcquire 方法首先调用 getState 方法获取 state 的值, 如果 state 的值为 0 (之前占领锁的线程刚好释放了锁), 那么用 CAS 这是 state 的值, 设置成功则将该线程设置成锁的所有者, 并且返回 true . 如果 state 的值不为 0 , 那就 调用
getExclusiveOwnerThread
方法查看占用锁的线程是不是自己 , 如果是的话那就直接将 state + 1 , 然后返回 true . 如果 state 不为 0 且锁的所有者又不是自己, 那就返回 false , 然后线程会进入到同步队列中 .
- final void lock() {
- //CAS 操作设置 state 的值
- if (compareAndSetState(0, 1))
- // 设置成功 直接将锁的所有者设置为当前线程 流程结束
- setExclusiveOwnerThread(Thread.currentThread());
- else
- // 设置失败 则进行后续的加入同步队列准备
- acquire(1);
- }
- public final void acquire(int arg) {
- // 调用子类重写的 tryAcquire 方法 如果 tryAcquire 方法返回 false 那么线程就会进入同步队列
- if (!tryAcquire(arg) &&
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
- }
- // 子类重写的 tryAcquire 方法
- protected final boolean tryAcquire(int acquires) {
- // 调用 nonfairTryAcquire 方法
- return nonfairTryAcquire(acquires);
- }
- final boolean nonfairTryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- // 如果状态 state=0, 即在这段时间内 锁的所有者把锁释放了 那么这里 state 就为 0
- if (c == 0) {
- // 使用 CAS 操作设置 state 的值
- if (compareAndSetState(0, acquires)) {
- // 操作成功 则将锁的所有者设置成当前线程 且返回 true, 也就是当前线程不会进入同步
- // 队列.
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- // 如果状态 state 不等于 0, 也就是有线程正在占用锁, 那么先检查一下这个线程是不是自己
- else if (current == getExclusiveOwnerThread()) {
- // 如果线程就是自己了, 那么直接将 state+1, 返回 true, 不需要再获取锁 因为锁就在自己
- // 身上了.
- int nextc = c + acquires;
- if (nextc <0) // overflow
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- // 如果 state 不等于 0, 且锁的所有者又不是自己, 那么线程就会进入到同步队列.
- return false;
- }
tryRelease 锁的释放
判断当前线程是不是锁的所有者, 如果是则进行步骤 2 , 如果不是则抛出异常.
判断此次释放锁后 state 的值是否为 0, 如果是则代表 锁有没有重入 , 然后将锁的所有者设置成 null 且返回 true , 然后执行步骤
3
, 如果不是则 代表锁发生了重入 执行步骤
4
.
现在锁已经释放完, 即 state=0 , 唤醒同步队列中的后继节点进行锁的获取.
锁还没有释放完, 即 state!=0 , 不唤醒同步队列.
- public void unlock() {
- sync.release(1);
- }
- public final boolean release(int arg) {
- // 子类重写的 tryRelease 方法, 需要等锁的 state=0, 即 tryRelease 返回 true 的时候, 才会去唤醒其
- // 它线程进行尝试获取锁.
- if (tryRelease(arg)) {
- Node h = head;
- if (h != null && h.waitStatus != 0)
- unparkSuccessor(h);
- return true;
- }
- return false;
- }
- protected final boolean tryRelease(int releases) {
- // 状态的 state 减去 releases
- int c = getState() - releases;
- // 判断锁的所有者是不是该线程
- if (Thread.currentThread() != getExclusiveOwnerThread())
- // 如果所的所有者不是该线程 则抛出异常 也就是锁释放的前提是线程拥有这个锁,
- throw new IllegalMonitorStateException();
- boolean free = false;
- // 如果该线程释放锁之后 状态 state=0, 即锁没有重入, 那么直接将将锁的所有者设置成 null
- // 并且返回 true, 即代表可以唤醒其他线程去获取锁了. 如果该线程释放锁之后 state 不等于 0,
- // 那么代表锁重入了, 返回 false, 代表锁还未正在释放, 不用去唤醒其他线程.
- if (c == 0) {
- free = true;
- setExclusiveOwnerThread(null);
- }
- setState(c);
- return free;
- }
公平锁的实现原理
lock 方法获取锁
获取状态的 state 的值, 如果 state=0 即代表锁没有被其它线程占用(但是并不代表同步队列没有线程在等待), 执行步骤 2 . 如果 state!=0 则代表锁正在被其它线程占用, 执行步骤
3
.
判断同步队列是否存在线程(节点), 如果不存在则直接将锁的所有者设置成当前线程, 且更新状态 state, 然后返回 true.
判断锁的所有者是不是当前线程, 如果是则更新状态 state 的值, 然后返回 true, 如果不是, 那么返回 false, 即线程会被加入到同步队列中
通过步骤 2 实现了锁获取的公平性, 即锁的获取按照先来先得的顺序, 后来的不能抢先获取锁, 非公平锁和公平锁也正是通过这个区别来实现了锁的公平性.
- final void lock() {
- acquire(1);
- }
- public final void acquire(int arg) {
- // 同步队列中有线程 且 锁的所有者不是当前线程那么将线程加入到同步队列的尾部,
- // 保证了公平性, 也就是先来的线程先获得锁, 后来的不能抢先获取.
- if (!tryAcquire(arg) &&
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
- }
- protected final boolean tryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- // 判断状态 state 是否等于 0, 等于 0 代表锁没有被占用, 不等于 0 则代表锁被占用着.
- if (c == 0) {
- // 调用 hasQueuedPredecessors 方法判断同步队列中是否有线程在等待, 如果同步队列中没有
- // 线程在等待 则当前线程成为锁的所有者, 如果同步队列中有线程在等待, 则继续往下执行
- // 这个机制就是公平锁的机制, 也就是先让先来的线程获取锁, 后来的不能抢先获取.
- if (!hasQueuedPredecessors() &&
- compareAndSetState(0, acquires)) {
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- // 判断当前线程是否为锁的所有者, 如果是, 那么直接更新状态 state, 然后返回 true.
- else if (current == getExclusiveOwnerThread()) {
- int nextc = c + acquires;
- if (nextc < 0)
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- // 如果同步队列中有线程存在 且 锁的所有者不是当前线程, 则返回 false.
- return false;
- }
tryRelease 锁的释放
公平锁的释放和非公平锁的释放一样, 这里就不重复.
公平锁和非公平锁的公平性是在 获取锁 的时候体现出来的, 释放的时候都是一样释放的.
lockInterruptibly 可中断方式获取锁
ReentrantLock 相对于 Synchronized 拥有一些更方便的特性, 比如可以中断的方式去获取锁.
- public void lockInterruptibly() throws InterruptedException {
- sync.acquireInterruptibly(1);
- }
- public final void acquireInterruptibly(int arg)
- throws InterruptedException {
- // 如果当前线程已经中断了, 那么抛出异常
- if (Thread.interrupted())
- throw new InterruptedException();
- // 如果当前线程仍然未成功获取锁, 则调用 doAcquireInterruptibly 方法, 这个方法和
- //acquireQueued 方法没什么区别, 就是线程在等待状态的过程中, 如果线程被中断, 线程会
- // 抛出异常.
- if (!tryAcquire(arg))
- doAcquireInterruptibly(arg);
- }
tryLock 超时等待方式获取锁
ReentrantLock 除了能以能中断的方式去获取锁, 还可以以超时等待的方式去获取锁, 所谓超时等待就是线程如果在超时时间内没有获取到锁, 那么就会返回 false , 而不是一直 "死循环" 获取.
判断当前节点是否已经中断, 已经被中断过则抛出异常, 如果没有被中断过则尝试获取锁, 获取失败则调用 doAcquireNanos 方法使用超时等待的方式获取锁.
将当前节点封装成独占模式的节点加入到同步队列的队尾中.
进入到 "死循环" 中, 但是这个死循环是有个限制的, 也就是当线程达到超时时间了仍未获得锁, 那么就会返回 false , 结束循环 . 这里调用的是
LockSupport.parkNanos
方法, 在超时时间内没有被中断, 那么线程会从 超时等待状态转成了就绪状态 , 然后被 CPU 调度继续执行循环, 而这时候线程已经达到超时等到的时间, 返回 false .
LockSuport 的方法能响应 Thread.interrupt , 但是不会抛出异常
- public boolean tryLock(long timeout, TimeUnit unit)
- throws InterruptedException {
- return sync.tryAcquireNanos(1, unit.toNanos(timeout));
- }
- public final boolean tryAcquireNanos(int arg, long nanosTimeout)
- throws InterruptedException {
- // 如果当前线程已经中断了 则抛出异常
- if (Thread.interrupted())
- throw new InterruptedException();
- // 再尝试获取一次 如果不成功则调用 doAcquireNanos 方法进行超时等待获取锁
- return tryAcquire(arg) ||
- doAcquireNanos(arg, nanosTimeout);
- }
- private boolean doAcquireNanos(int arg, long nanosTimeout)
- throws InterruptedException {
- if (nanosTimeout <= 0L)
- return false;
- // 计算超时的时间 即当前虚拟机的时间 + 设置的超时时间
- final long deadline = System.nanoTime() + nanosTimeout;
- // 调用 addWaiter 将当前线程封装成独占模式的节点 并且加入到同步队列尾部
- final Node node = addWaiter(Node.EXCLUSIVE);
- boolean failed = true;
- try {
- for (;;) {
- final Node p = node.predecessor();
- // 如果当前节点的前驱节点为头结点 则让当前节点去尝试获取锁.
- if (p == head && tryAcquire(arg)) {
- // 当前节点获取锁成功 则将当前节点设置为头结点, 然后返回 true.
- setHead(node);
- p.next = null; // help GC
- failed = false;
- return true;
- }
- // 如果当前节点的前驱节点不是头结点 或者 当前节点获取锁失败,
- // 则再次判断当前线程是否已经超时.
- nanosTimeout = deadline - System.nanoTime();
- if (nanosTimeout <= 0L)
- return false;
- // 调用 shouldParkAfterFailedAcquire 方法, 告诉当前节点的前驱节点 我要进入
- // 等待状态了, 到我了记得喊我, 即做好进入等待状态前的准备.
- if (shouldParkAfterFailedAcquire(p, node) &&
- nanosTimeout> spinForTimeoutThreshold)
- // 调用 LockSupport.parkNanos 方法, 将当前线程设置成超时等待的状态.
- LockSupport.parkNanos(this, nanosTimeout);
- if (Thread.interrupted())
- throw new InterruptedException();
- }
- } finally {
- if (failed)
- cancelAcquire(node);
- }
- }
ReentrantLock 和 Synchronized 对比
关于 Synchronized 的介绍可以看 《synchronized 的使用(一)》 https://ddnd.cn/2019/03/21/java-synchronized/ , 《深入分析 synchronized 原理和锁膨胀过程(二)》 https://ddnd.cn/2019/03/22/java-synchronized-2/
ReentrantLock | Synchronized | |
---|---|---|
底层实现 | 通过 AQS 实现 | 通过 JVM 实现,其中 synchronized 又有多个类型的锁,除了重量级锁是通过 monitor 对象 (操作系统 mutex 互斥原语) 实现外,其它类型的通过对象头实现。 |
是否可重入 | 是 | 是 |
公平锁 | 是 | 否 |
非公平锁 | 是 | 是 |
锁的类型 | 悲观锁、显式锁 | 悲观锁、隐式锁(内置锁) |
是否支持中断 | 是 | 否 |
是否支持超时等待 | 是 | 否 |
是否自动获取 / 释放锁 | 否 | 是 |
参考
《Java 并发编程的艺术》
深入理解 AbstractQueuedSynchronizer(AQS)
Java 重入锁 ReentrantLock 原理分析) https://www.imooc.com/article/28934
来源: http://www.tuicool.com/articles/2EfUvuf