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 voidlock() {

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(intarg) {

if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

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

final boolean nonfairTryAcquire(intacquires) {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,于是我们贴出这个方法涉及的源码:

privateNode addWaiter(Node mode) {

Node node= newNode(Thread.currentThread(), mode);// 在这里首先根据当前线程创建出一个节点

Node pred =tail;// 既然要插入节点,肯定是要插入到最尾部的,先获取到tail节点if (pred != null) {

node.prev=pred;// 将当前节点的prev和尾部节点关联 --第五行if(compareAndSetTail(pred, node)) {

pred.next=node;// node和老tail关联完成returnnode;

}

}

enq(node);returnnode;

}

如果某个线程在插入队列没有其他线程干扰的话,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(finalNode node) {// 此时没有成功CAS的C节点“失魂落魄”的走了进来for(;;) {

Node t=tail;if (t == null) { //

if (compareAndSetHead(newNode()))// 如果此时队列完全为空(第一个线程进来),需要弄一个冗余head节点,之后你会看到作用的。。别急

tail=head;

}else{

node.prev=t;// 此时的C节点要和B节点绑上关系if(compareAndSetTail(t, node)) {

t.next=node;// 关联完成returnt;

}

}

}

}

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

final boolean acquireQueued(final Node node, intarg) {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;returninterrupted;

}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())

// 判断当前节点是否可以被阻塞,shouldParkAfterFailedAcquire方法为核心

interrupted= true;

}

}finally{if(failed)

cancelAcquire(node);

}

}

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

private static booleanshouldParkAfterFailedAcquire(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(intarg) {if(tryRelease(arg)) {

Node h=head;if (h != null && h.waitStatus != 0)

unparkSuccessor(h);// 关键操作,如何去唤醒后面的小伙伴return true;

}return false;

}

protected final boolean tryRelease(intreleases) {int c = getState() -releases;if (Thread.currentThread() !=getExclusiveOwnerThread())throw newIllegalMonitorStateException();boolean free = false;if (c == 0) {

free= true;

setExclusiveOwnerThread(null);// 彻底释放锁后,将ownerThread设置为null,重置state

}

setState(c);returnfree;

}

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

private voidunparkSuccessor(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一定是0if (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源码_Java-AQS源码详解(细节很多!)相关推荐

  1. java单例设计模式_Java设计模式之单例模式详解

    在Java开发过程中,很多场景下都会碰到或要用到单例模式,在设计模式里也是经常作为指导学习的热门模式之一,相信每位开发同事都用到过.我们总是沿着前辈的足迹去做设定好的思路,往往没去探究为何这么做,所以 ...

  2. java 打印异常内容_java自定义异常打印内容详解

    本文实例为大家分享了java自定义异常打印内容的具体代码,供大家参考,具体内容如下 背景:在开发中,我们可能会使用到自定义异常,但是,这个自定义异常在打印日志时,往往打印的内容比较多. 1.自定义异常 ...

  3. java虚拟机工作原理_Java虚拟机工作原理详解

    一.类加载器 首先来看一下java程序的执行过程. 从这个框图很容易大体上了解java程序工作原理.首先,你写好java代码,保存到硬盘当中.然后你在命令行中输入: javac YourClassNa ...

  4. java代码轻量级锁_Java轻量级锁原理详解(Lightweight Locking)

    转自http://www.cnblogs.com/redcreen/archive/2011/03/29/1998801.html 大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的 ...

  5. java的static类_java中staticclass静态类详解

    一般情况下是不可以用static修饰类的.如果一定要用static修饰类的话,通常static修饰的是匿名内部类. 在一个类中创建另外一个类,叫做成员内部类.这个成员内部类可以静态的(利用static ...

  6. Java 初始化 代码块_Java中初始化块详解及实例代码

    Java中初始化块详解 在Java中,有两种初始化块:静态初始化块和非静态初始化块. 静态初始化块:使用static定义,当类装载到系统时执行一次.若在静态初始化块中想初始化变量,那仅能初始化类变量, ...

  7. java connection 单例_Java设计模式之单例模式详解

    Java设计模式之单例模式详解 什么是设计模式 设计模式是在大量的实践中总结和理论之后优选的代码结构,编程风格,以及解决问题的思考方式.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可 ...

  8. java指数表示法_Java指数计数法详解

    Java指数计数法详解 时间:2017-10-16     来源:华清远见Java培训中心 Java指数计数法并不是一个很难的运算,关键是你要理解应用,很多朋友不理解Java指数计数法,所以也无从运用 ...

  9. java方法怎么写_java方法定义格式详解,java方法怎么写?

    对于java方法你了解多少呢?你知道java方法应该如何写吗?下面要给大家介绍的就是和java方法相关的内容,一起来了解一下这个概念吧. 在学习运算符的时候,都为每个运算符单独的创建一个新的类和mai ...

  10. java解析json数据_java解析JSON数据详解

    JSON是目前最流行的轻量级数据交换语言(没有之一).尽管他是javaScript的一个子集.但由于其是独立与语言的文本格式,它几乎可以被所有编程语言所支持. 以下是对java语言中解析json数据的 ...

最新文章

  1. vscode 无法跳转到函数定义_玩转VS Code
  2. mysql python 接口_Python中的MySQL接口:PyMySQL MySQLdb
  3. 安装安卓SDK和JDK的简便方法
  4. INLINE HOOK过驱动保护的理论知识和大概思路
  5. Linux环境PHP7安装
  6. 【HDU - 4345 】Permutation(DP)
  7. asp.net页面调用cs中的方法
  8. 适配器模式的极简概述
  9. windows库的创建和使用:静态库+动态库
  10. 带蓝色的紫罗兰色——三色配色篇
  11. D. Pythagorean Triples (math、暴力)
  12. 影音视频领域开源项目专区
  13. 如何在Nginx上 安装SSL证书
  14. nodejs 安装模块失败 解决方法
  15. VS2019 .NetCore智能提示从英文变成中文设置
  16. mongoose http 源码解析(1)
  17. python中对象的多态、封装、继承介绍
  18. Rtools is required to build R packages but is not currently installed
  19. 美洽消息推送 php,GitHub - Meiqia/MeiqiaSDK-Push-Signature-Example: 美洽 SDK 3.0 推送的数据结构签名算法,多语言示例。...
  20. 【MySQL】——mysql exporter源码分析

热门文章

  1. CUDA运行时 Runtime(二)
  2. 人脸照片自动生成游戏角色_ICCV2019论文解析
  3. 2021年大数据Spark(十七):Spark Core的RDD持久化
  4. java gui 案例_JavaGui入门—布局的嵌套使用附实例
  5. HarmonyOS Text超出部分末尾显示...
  6. ValueError: max() arg is an empty sequence
  7. Windows 系统下使用grep 命令
  8. github READme 的使用教程
  9. JS中的7种设计模式
  10. 随心测试_软测基础_005 测试人员工作内容