ReentrantLock调用lock()时时序图:

addWaiter方法:

enq方法:自旋

  它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

  aqs有两种资源访问模式:独占(ReentrantLock)和共享(CountDownLatch和Semaphore、CyclicBarrier)

  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了!接下来开始撸吧。。至于这里双向链表是怎么样的一个结构,这里就不做多于的描述了,大家可以自行去补充。

  

  首先我们由一张图开头,我们要知道AQS其实主要实现的是一个FIFO的双向链表的维护,每个Node其实就是一个等待被释放的线程,在竞争锁失败后,会封装成Node的形式进入到链表尾部。。在了解了最基本的概念后,我们先来看看AQS最经典的应用ReentrantLock的lock方法:

public void lock() {
sync.lock();// sync主要两种实现类
}
// 第一种非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L; /**非公平锁实现的lock方法
*/
final void lock() {
if (compareAndSetState(0, 1))// CAS操作去尝试将state变为1,也就是独占状态
setExclusiveOwnerThread(Thread.currentThread());// 非公平锁并不会老老实实去排队,而是一上来就插队,插不了就只能去排队了。。
else
acquire(1);
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 第二种公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L; final void lock() {
acquire(1);// 相比于非公平锁,就比较守规矩了
}

  因为非公平和公平就只有这么一个差别,那我就以非公平锁为切入点了,可以看到在尝试抢占失败后,调用acquire方法,ok进入到该方法:

// 此方法是AQS的,但是注意里面的tryAcquire是需要我们的自定义AQS实现的,直接调用AQS的会直接抛出异常UnsupportedOperationException
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

  tryAcquire是NonfairSync实现的,而他内部又直接调用Sync父类的nonfairTryAcquire方法:

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);// 直接CAS独占
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;// 这里很确切的说明了ReentrantLock是一个可重入的锁
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

  上面的方法相信大家应该很快就理解了,在尝试独占失败后,tryAcquire操作返回false,然后这个时候要做的操作相信大家也可以猜到,就是插入到双向链表中,看上面的代码,第一个操作是addWaiter,于是我们贴出这个方法涉及的源码:

private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);// 在这里首先根据当前线程创建出一个节点
Node pred = tail;// 既然要插入节点,肯定是要插入到最尾部的,先获取到tail节点
if (pred != null) {
node.prev = pred;// 将当前节点的prev和尾部节点关联 --第五行
if (compareAndSetTail(pred, node)) {
pred.next = node;// node和老tail关联完成
return node;
}
}
enq(node);
return node;
}

  如果某个线程在插入队列没有其他线程干扰的话,enq都不会进去的,直接在CAS设置成tail之后直接返回了,但是实际上,总是会有那么几个“不长眼”的线程来和你对着干。。。来假设这么一个场景:A线程是tail节点,此时B和C进来,他们都同时进入到第五行那里,也就是你会发现A会有B和C两个节点的prev指向它,但是下一行的CAS操作是一个原子性操作,所以B和C只能一个成为tail,那么又假设B成功CAS了,也就是B可以直接返回,但是C就比较“悲催”了,它得进入到下一个方法enq,因为此时的链表结构很是奇怪,C的prev指向了old tail:A,所以得做一个“修复”结构操作,将C的prev指向B,接下来看enq代码:

private Node enq(final Node node) {// 此时没有成功CAS的C节点“失魂落魄”的走了进来
for (;;) {
Node t = tail;
if (t == null) { //
if (compareAndSetHead(new Node()))// 如果此时队列完全为空(第一个线程进来),需要弄一个冗余head节点,之后你会看到作用的。。别急
tail = head;
} else {
node.prev = t;// 此时的C节点要和B节点绑上关系
if (compareAndSetTail(t, node)) {
t.next = node;// 关联完成
return t;
}
}
}
}

  此时的C应该是可以回到正轨的,就算此时又一个线程打扰了C的关联操作而导致CAS失败,但是因为代码在for循环里,可以重试,基本上很快就可以回到队列正轨!!于是我们又可以愉快的进行下一个步骤了,再回到我们熟悉的acquire(有点绕,忘记的往上翻),可以看到addWaiter之后,会将当前节点返回给一个“新面孔”-acquireQueued方法作为参数,我们再看看这个方法是怎么做的:

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)) {
            // 当前置节点为head,那么可以去尝试获取锁,成功的话就调用setHead方法将自己设置为head节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
            // 判断当前节点是否可以被阻塞,shouldParkAfterFailedAcquire方法为核心
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  可以看到,如果当前节点的上一个节点就是head的话,说明当前可以竞争到锁的概率会很大,一旦head节点的线程执行完unlock后,当前的state变为0,当前节点就可以进入到setHead方法,但是如果头节点还在执行中,那么当前节点只能老老实实的进入到shouldParkAfterFailedAcquire方法内部,来决定当前节点是否应该能被阻塞:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//static final int CANCELLED = 1;
      static final int SIGNAL = -1;
      static final int CONDITION = -2;
      static final int PROPAGATE = -3;
      int ws = pred.waitStatus;// 在这里终于有用上这个变量了
      if (ws == Node.SIGNAL)
       /* 在这里我打算用一个很易于理解的方式来讲述这个SIGNAL值有什么用:
        相信大家都有过排队的经历,在这里服务窗口相当于锁,每个人过来时,发现窗口有其他人在,所以此时只能去队尾排队,也就是addWaiter操作,在队尾后,waitStatus的值默认是0,但是此时刚排进队的小伙伴,因为队伍太长,
        而且比较累,需要低头打个盹,但是怕如果瞌睡打过头了,就不知道什么时候窗口没人了可以被服务,所以此时小伙伴为了保险,他需要一个可靠的“前置队友”,也就是他前面的人如果业务办完了,可以顺便回头来叫醒他,在这里可
        以把“委托前面的人,如果结束了麻烦叫醒我,谢谢!”这个操作理解为将prev节点的waitStatus设置为SIGNAL,如果前置节点的waitStatus不是0,需要尝试设置为SIGNAL,但如果前面的小伙伴已经是SIGNAL了,直接返回,
        说明当前小伙伴可以安心的打盹了(被阻塞)!!
*/
return true;
  if (ws > 0) {
/*
* 如果是CANCELLED,代表当前节点已经不需要处理业务了,可以在队列里直接清除出去,然后队列重新规整
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
 } else {
/* 到这一步,就会尝试去将前置节点设置为SIGNAL,但是有可能会设置失败或者设置成功,但是不论成功还是失败,都会返回false,也就是在上面的acquireQueued中,返回false后会继续for循环里去尝试获取锁,
         因为小伙伴必须要确定前面的伙伴要靠谱,也就是必须要是SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

  我们再来看看unlock方法,他有直接调用AQS的release方法,而tryRelease方法由自定义的AQS类实现:

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// 关键操作,如何去唤醒后面的小伙伴
return true;
}
return false;
}
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);// 彻底释放锁后,将ownerThread设置为null,重置state
}
setState(c);
return free;
}

  tryRelease操作其实很好理解,主要是unparkSuccessor方法:

private void unparkSuccessor(Node node) {
/*
* 在释放完锁后,此时的节点他已经不需要SIGNAL这个状态了,因为他觉得自己办完业务了,就可以尝试去给自己“放个假”,当变成0的时候,后面的小伙伴在shouldPark里就会返回false,代表当前前置节点很有可能不是刚刚初始化
      导致的waitStatus == 0,而是前置节点刚释放完锁,所以就是head:“我此时已经释放完锁了,后面的,你现在就别打盹了,赶紧再去尝试抢锁吧!”,于是此时心急的小伙伴就赶紧再进入for循环里尝试tryAcquire
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); /*
*
      */
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
          // 从后往前,找到第一个需要被唤醒的小伙伴,状态也是必须>=0,至于为什么会==0,因为最后一个节点的status一定是0
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);// 此时的s就是下一个需要被唤醒的,于是unpark
}

  不知道你们有没有发现,为什么上面的代码里要从后往前扫描呢,双向链表不是两边都可以扫吗,这个就很有趣了,不知道你们有没有看到在addWaiter和enq方法里,在将当前节点CAS成tail的前一步,有一个先将node的prev设置为前一个节点,也就是双向表的建立关系是先后节点连接前节点开始的,但是因为设置两个节点的关系时不是原子操作,那么就会导致可能prev关系存在,但是next关系不存在的时候,unpark操作就开始需要去遍历链表了,而这个时候,用next操作就很可能会遗漏掉哪个“小伙伴”而导致出现误“唤醒”!!

Java-AQS源码详解(细节很多!)的更多相关文章

  1. JUC锁:核心类AQS源码详解

    目录 1 疑点todo和解疑 2 AbstractQueuedSynchronizer学习总结 2.1 AQS要点总结 2.2 细节分析 2.2.1 插入节点时先更新prev再更新前驱next 2.2 ...

  2. Java HashMap源码详解

    Java数据结构-HashMap 目录 Java数据结构-HashMap 1. HashMap 1.1 HashMap介绍 1.1.1 HashMap介绍 1.1.2 HashMap继承图 1.2 H ...

  3. Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节

    简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...

  4. 数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解

    数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解 对数组有不了解的可以先看看我的另一篇文章,那篇文章对数组有很多详细的解析,而本篇文章则着重讲动态数组,另一篇文章链接 ...

  5. Java源码详解系列(十)--全面分析mybatis的使用、源码和代码生成器(总计5篇博客)

    简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

  6. Shiro 登录认证源码详解

    Shiro 登录认证源码详解 Apache Shiro 是一个强大且灵活的 Java 开源安全框架,拥有登录认证.授权管理.企业级会话管理和加密等功能,相比 Spring Security 来说要更加 ...

  7. 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码

    简介 HikariCP 是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,和 dr ...

  8. Activiti架构分析及源码详解

    目录 Activiti架构分析及源码详解 引言 一.Activiti设计解析-架构&领域模型 1.1 架构 1.2 领域模型 二.Activiti设计解析-PVM执行树 2.1 核心理念 2. ...

  9. 源码详解系列(七) ------ 全面讲解logback的使用和源码

    什么是logback logback 用于日志记录,可以将日志输出到控制台.文件.数据库和邮件等,相比其它所有的日志系统,logback 更快并且更小,包含了许多独特并且有用的特性. logback ...

  10. RocketMQ源码详解 | Broker篇 · 其三:CommitLog、索引、消费队列

    概述 上一章中,已经介绍了 Broker 的文件系统的各个层次与部分细节,本章将继续了解在逻辑存储层的三个文件 CommitLog.IndexFile.ConsumerQueue 的一些细节.文章最后 ...

随机推荐

  1. 【QT学习笔记】二、信号槽和自定义信号槽

    1. 信号槽 int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton button("Qui ...

  2. Ubuntu18.04安装MySQL与默认编码设置

    安装 打开终端直接开始,编码配置方法在后面 #通过apt更新包索引 sudo apt update #按照默认软件包安装 sudo apt install mysql-server #运行安全脚本 s ...

  3. 【php设计模式】门面模式

    门面模式又叫外观模式,用来隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口.这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性. 这种模式涉及到一个单一的类 ...

  4. .NET 反射应用

    object request = null; string requestObjClassName = "命名空间" + 类型.ToString(); Type type = Ty ...

  5. 内核开机logo

    安装 # sudo apt-getinstall netpbm 制图 # bmptoppm logo.bmp > logo.ppm # ppmquant 224 logo.ppm > lo ...

  6. flask开发环境

    1. 创建虚拟环境flask_py3 虚拟环境是一个互相隔离的目录 mkvirtualenv flask_py3 2.安装flask包 pip install flask==0.10.1 其他:导入f ...

  7. sql 随机数系列

    一.把数据库把某个字段更新为随机数 DECLARE @Hour INT DECLARE @Counts INT SET @Hour =DATENAME(HOUR, GETDATE()) ) BEGIN ...

  8. C# 填充无效,无法被移除

    1.本文采用微软的 RijndaelManaged 命名空间: System.Security.Cryptography Assemblies: mscorlib.dll, netstandard.d ...

  9. 【Java并发】锁机制

    一.重入锁 二.读写锁 三.悲观锁.乐观锁 3.1 悲观锁 3.2 乐观锁 3.3 CAS操作方式 3.4 CAS算法理解 3.5 CAS(乐观锁算法) 3.6 CAS缺点 四.原子类 4.1 概述 ...

  10. Appium&Java自动化实现移动端几种典型动作

    一.Appium4.0 Pinch&Zoom /* * @FileName Pinch_Zoom: Pinch_Zoom * @author davieyang * @create 2018- ...