ReentrantLock.Sync#tryRelease
该方法也体现了锁重入次数的操作,源代码如下:
protected final boolean tryRelease(int releases) { // 当前锁线程重入次数减去要释放的次数 int c = getState() - releases; // 当前线程不等于锁持有线程,则判断中断监听锁异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 若减去后的锁次数为 0 if (c == 0) { // 返回 true、设置锁持有线程为 null,其他线程就可以竞争锁了 free = true; setExclusiveOwnerThread(null); } // 递减锁重入次数,返回 false,锁仍然被当前线程所持有 setState(c); return free; }
ReentrantLock.Sync#tryRelease 执行流程主要分析如下:
- 通过将 AQS#state 属性值减少传入的参数值(参数:1)若减去的结果状态值为 0,就将排它锁 Owner 持有线程设置为 null,同时返回 true,以便于其他的线程有机会执行竞争锁操作
- 若减去的结果状态值不为 0,返回 free 变量默认值 false,当前线程仍然继续持有这把锁,其他线程暂时不可以争抢锁
在排它锁中,加锁时 state 状态会增加 1,在解锁时会减去 1,同一把锁,在被重入时,可能会被叠加为 2、3、4 等,只有当调用 unlock 方法次数与调用 lock 方法次数相对应,才会把锁 Owner 持有线程设置为空,也只有这种情况下该方法执行结果才有返回 true
AQS#unparkSuccessor
当 ReentrantLock.Sync#tryRelease 方法执行完以后,会取同步等待队列中首节点,唤醒队列中下一个节点去争抢这把锁,该方法源码如下:
private void unparkSuccessor(Node node) { // 获取传入节点的 waitStatus 属性值 int ws = node.waitStatus; if (ws < 0) // 小于 0 通过 CAS 将其修改为 0 compareAndSetWaitStatus(node, ws, 0); // 获取传入节点的后继节点 Node s = node.next; // 若后继节点为空或者 waitStatus 大于 0 说明它是 CANCELLED-结束状态 if (s == null || s.waitStatus > 0) { s = null; // 从尾部节点开始扫描,找到距离当前传入节点最近的一个 waitStatus 小于等于 0 的节点 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } // 将同步等待队列中 > 最前面的一个非 CANCELLED 状态的 Node 线程进行唤醒 if (s != null) LockSupport.unpark(s.thread);
分析一下此方法分别会作那些事情,如下:
- 获取当前传入节点 Node waitStatus 属性值,若它小于 0 时,先通过 CAS 操作将其修改为 0
当前节点其实就是 head 头节点,唤醒的操作不会唤醒头节点,只会唤醒头节点后面不为 CANCELLED 状态的首节点 Node 线程
- 获取当前节点的 next 后继节点,若后继节点为空或 waitStatus 大于 0(CANCELLED)那么就会遍历该同步等待队列,从尾部往前查找的方式,匹配到与当前节点最近的一个非 CANCELLED 节点,将其设置为待唤醒的节点
- 若待唤醒的节点不为空,调用原生锁 LockSupport#unpark 方法将其唤醒,以便它可以再次去争抢锁
当节点被唤醒后,比如:Thread-A 释放锁成功以后,会调用 AQS#unparkSuccessor 方法唤醒它的下一个节点 Thread-B 所持有的(非 CANCELLED)
随机 Thread-B 被唤醒,它会继续执行 AQS#acquireQueued 方法中的循环,执行:if (p == head && tryAcquire(arg)) 代码块,所以后续被唤醒的线程都会是这样,通过该代码来确保同步队列中的节点能够获取锁资源
那么为什么在释放锁的时候一定要从尾部开始扫描呢?
回顾一下 AQS#enq 方法执行的逻辑,插入新节点时,它是从队列尾部进行节点入队的,
看下图红色所标注的
,在 CAS 操作成功之后,t.next = node; 操作之前,可能会存在其他线程调用 unlock 方法从 head 开始向后遍历,由于 t.next = node; 还未执行也就意味着同步等待队列关系还未建立完整,就会导致遍历到原始的尾部节点时被中断 > 队列中的链表关系断链了;所以说,从后往前遍历就不会出现这个问题
挂起线程被唤醒后执行过程
当持有锁的线程调用 ReentrantLock#unlock 方法,原本被挂起的 Thread-B、Thread-C 线程就有机会被唤醒再继续执行,被唤醒之后的线程会继续执行 AQS#acquireQueued 方法内的循环,该方法在上面已经分析过了,接下来以 Thread-B 被唤醒后为例,看它整个的执行过程以及变化,以流程图的方式呈现
同步等待队列变更结构图:
同步等待队列执行过程流程图:
博主是以如下源码,对 ReentrantLock、AQS 核心方法源码进行查看的,分享如下:
public class MultiThreadReentrantLockDemo { private static final ReentrantLock LOCK = new ReentrantLock(); public void threadAProcess() { LOCK.lock(); try { System.out.println("执行:threadAProcess 方法"); // 处理业务逻辑中.... // 断点过程中该时间可以延长 TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); } finally { LOCK.unlock(); } } public void threadBProcess() { LOCK.lock(); try { System.out.println("执行:threadBProcess 方法"); // 处理业务逻辑中.... TimeUnit.SECONDS.sleep(60 * 5); } catch (InterruptedException e) { e.printStackTrace(); } finally { LOCK.unlock(); } } public void threadCProcess() { LOCK.lock(); try { System.out.println("执行:threadCProcess 方法"); // 处理业务逻辑中.... TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } finally { LOCK.unlock(); } } public static void main(String[] args) throws InterruptedException { MultiThreadReentrantLockDemo multiThreadLock = new MultiThreadReentrantLockDemo(); new Thread(()-> multiThreadLock.threadAProcess(), "Thread-A").start(); Thread threadB = new Thread(() -> multiThreadLock.threadBProcess(), "Thread-B"); threadB.start(); // 可能会出现 Thread-C 先执行的情况,所以先通过 join 方法让线程 B 先跑完 threadB.join(); new Thread(()-> multiThreadLock.threadCProcess(), "Thread-C").start(); } }
注意:断点模式下,要以 Thread 模式执行;如下图:
公平锁、非公平锁区别
锁的公平与否其实取决于获取锁的顺序性,若为公平锁,那么获取锁的顺序应该绝对符合 FIFO 队列 > 先进先出的特性,上面所分析的例子都是以非公平锁(默认是非公平锁)只要 CAS 设置 AQS#state 属性值成功,就代表当前线程获取到了锁,而公平锁不一样,差异的地方有如下两点:
1、FairSync#lock、NonfairSync#lock
非公平锁在获取锁时,先通过 CAS 操作进行锁抢占,而公平锁不会
2、FairSync#tryAcquire、NonfairSync#tryAcquire
两者方法之间不同之处在于判断多了一个条件:hasQueuedPredecessors,也就是说加入了同步队列中当前节点是否有前驱节点的判断,若该方法返回 true,则表示有线程比当前线程更早入队、更早地请求获取锁,因此,需要等待前驱节点的线程获取完再释放锁以后才能继续获取锁!
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
1、h != t:头尾节点是否相同,若相同则表示队列中只有一个节点,即当前未发生锁竞争
假设:当前只有线程 Thread-A 一人争抢锁,那么 head == null && tail ==null,那么返回 false 会去争抢锁;反之,会继续走第二步的判断
2、(s = h.next) == null:检查头节点的后继节点是否为空,即判断是否存在后继节点
假设:线程 Thread-A、Thread-B 同时争抢锁,Thread-A 抢到了,那么同步等待队列中会有头节点、Thread-B 所在节点,条件不满足返回 false,会继续走第三步的判断;反之,当前只有一个节点 > 返回 true 不会去争抢锁,不走第三步的判断
3、s.thread != Thread.currentThread():检查后继节点的线程是否与当前线程不同,即判断后继节点持有线程是否为当前线程
假设:头节点的后继节点持有线程就是当前的线程,会返回 false 会去争抢锁;反之,头节点的后继节点持有线程不是当前的线程,会返回 true 不会去争抢锁,它会进入到排队模式!!!
总结
ReentrantLock 基于悲观锁实现(LockSupport),但是在处理 AQS#state 锁状态时是基于 CAS 乐观锁实现的,两者在不同场景下都会各自的好处,因为前者已经悲观锁,后者再用 CAS 操作并没有任何问题
>在这里其实就是偷换概念了,不一定用了悲观锁就不能用乐观锁
该篇博文介绍了 JUC 组件下 ReentrantLock 核心概念、使用、源码以及 AQS 基础组件的核心方法,阐述了 AQS 内部实现、数据结构以及节点变更过程,在后面,看 ReentrantLock 是如何基于 AQS 核心方法去完成其内部锁竞争工作的、锁释放后如何唤醒其他节点线程,全文上下以画图+文字加以说明,不限于时序图、结构图、流程图,最后说明了在 ReentrantLock 公平锁、非公平锁之间的区别,希望能够帮助你快速理解 AQS 内部如何巧妙处理高并发场景问题的
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!