什么是AbstractQueuedSynchronizer(AQS)

字面意思是抽象队列同步器,使用一个voliate修饰的int类型同步状态,通过一个FIFO队列完成资源获取的排队工作,把每个参与资源竞争的线程封装成一个Node节点来实现锁的分配。

AbstractQueuedSynchronizer源码

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;//链表头
private transient volatile Node tail;//链表尾
private transient Thread exclusiveOwnerThread;//持有锁的线程
private volatile int state;//同步状态,0表示当前没有线程获取到锁
static final class Node {//链表的Node节点类
volatile int waitStatus;//当前节点在队列中的状态
volatile Node prev;//前置节点
volatile Node next;//后置节点
volatile Thread thread;//当前线程
}
}

AQS同步队列的基本结构

Node.waitStatus的说明

状态值 描述
0 节点默认的初始值
SIGNAL=-1 线程已经准备好,等待释放资源
CANCELLED=1 获取锁的请求线程被取消
CONDITION=-2 节点在队列中,等待唤醒

state为什么要用volatile修饰?

  1. 可见性,一个线程对变量的修改可以立即被别的线程感知到
  2. 有序性,禁止指令重排

AQS获取锁步骤

  public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
  1. 当一个线程获取锁时,首先判断state状态值是否为0
  2. 如果state==0,则通过CAS的方式修改为非0状态
  3. 修改成功,则表明获取锁成功,执行业务代码
  4. 修改失败,则把当前线程封装为一个Node节点,加入到队列中并挂起当前线程
  5. 如果state!=0,则把当前线程封装为一个Node节点,加入到队列中并挂起当前线程

AQS获取锁过程

首先调用tryAcquire去修state的状态值,成功就获取当前锁;失败则加入当前等待队列中,然后挂起线程。

tryAcquire

AQS的源码中tryAcquire是一空实现,需要它的子类去实现这个空方法。因为在AQS中虽然公平锁非公平锁的都是基于一个CLH去实现,但是在获取锁的过程中略有不同。

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

公平锁FairSync#tryAcquire

protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
int c = getState();//获取同步器的状态
if (c == 0) {//当前没有线程获取到锁
//首先判断祖宗节点的线程是否当前线程一样
if (!hasQueuedPredecessors() &&
//更改state的状态值为非0
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果锁持有者的线程是当前线程,则可放行,锁的重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
/**
* 判断祖宗节点的线程是否当前线程一样
* 傀儡节点的下个节点
*/
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//头节点的下个节点所持有的线程是否与当前线程相同
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

非公平锁NonfairSync#tryAcquire

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//通过CAS更改state的状态值
if (compareAndSetState(0, acquires)) {
//把当前线程设置为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
//如果锁持有者的线程是当前线程,则可放行,锁的重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}



对比后发现,公平锁先判断是否有老祖宗节点,如果有则返回false;如果当前线程对应的node就是老祖宗节点,则直接去修改state状态,把state改为非0。

addWaiter

获取锁成功的线程去执行业务逻辑了,获取锁失败的线程则会在队列中排队等候,每个等候的线程也都不安分的。

private Node addWaiter(Node mode) {
//把当前线程封装为一个Node节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//加入到队列的尾部
enq(node);
return node;
}
  1. 把当前线程封装为一个Node节点
  2. 当第一次执行这个方法时,由于head和tail都还没有赋值,则pred指向的tail也是空,所以直接直到enq(node)
  3. 当pred指向的tail不为空时,则通过CAS的方式加入到尾部,如果成功直接返回;如果失败,则进入enq(node)通过自旋的方式加入。
//通过自旋的方式将节点加入到节点的尾部
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

为了操作链表的方便,一般都要在链表的头前加入一个傀儡节点,AQS的链表也不例外。

先创建一个傀儡节点,并把head、tail均指向它,然后再把node节点加入到尾部后面,移动tail的指向。

acquireQueued

当节点成功加入到链表的尾部后,等待被唤醒,然后通过自旋的方式去获取锁

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//当前节点的前置节点
final Node p = node.predecessor();
//如果前置节点是傀儡节点(head指向傀儡节点),则再次尝试去获取锁
if (p == head && tryAcquire(arg)) {
//获取成功后,则移除之前的傀儡节点,head指向当前node,
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//获取锁失败后,设置node节点的状态,并挂起当前节点
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
  1. 获取node节点的前置节点,如果前置节点是head,则再次尝试去获取锁
  2. 设置当前node节点的前置节点状态为-1(表示后续节点正在等待状态,默认是0),然后通过自旋的后会进行到parkAndCheckInterrupt挂起当前节点
  3. LockSupport.park(this)执行完事,当前线程会一直阻塞到这个地方
  4. 当前唤醒时再次从1开始执行

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;
}

主要是恢复state的值、重置锁持有都线程,然后唤醒挂起的线程。

protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//当前线程与锁持有者线程不一样会报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//重入的次数为0时,则当前线程已经没有重入了,可以清空锁的持有者
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

恢复state状态的值,如果重入次数为0时,则清空锁的持有都为null

private void unparkSuccessor(Node node) {
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)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)//唤醒下个node对应的线程
LockSupport.unpark(s.thread);
}

设置head指向的node节点的watiStatus的状态值,然后找到下个节点对应的线程并唤醒。

【一知半解】AQS的更多相关文章

  1. 深入浅出AQS源码解析

    最近一直在研究AQS的源码,希望可以更深刻的理解AQS的实现原理.虽然网上有很多关于AQS的源码分析,但是看完以后感觉还是一知半解.于是,我将自己的整个理解过程记录下来了,希望对大家有所帮助. 基本原 ...

  2. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  3. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  4. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  5. 【Java并发编程实战】----- AQS(一):简介

    在前面博客中,LZ讲到了ReentrantLock.ReentrantReadWriteLock.Semaphore.CountDownLatch,他们都有各自获取锁的方法,同时相对于Java的内置锁 ...

  6. 获取文件的缩略图Thumbnail和通过 AQS - Advanced Query Syntax 搜索本地文件

    演示如何获取文件的缩略图 FileSystem/ThumbnailAccess.xaml <Page x:Class="XamlDemo.FileSystem.ThumbnailAcc ...

  7. 基于ReentrantLock的AQS的源码分析(独占、非中断、不超时部分)

    刚刚看完了并发实践这本书,算是理论具备了,看到了AQS的介绍,再看看源码,发现要想把并发理解透还是很难得,花了几个小时细分析了一下把可能出现的场景尽可能的往代码中去套,还是有些收获,但是真的很费脑,还 ...

  8. 基于AQS的锁

    锁分为独占锁和共享锁,它们的主要实现都是依靠AbstractQueuedSynchronizer,这个类只提供一系列公共的方法,让子类来调用.基于我了解不深,从这个类的属性,方法,和独占锁的获取方式去 ...

  9. Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析

    经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...

随机推荐

  1. 看看JDK1.7与1.8的内存模型差异

    JDK1.7与1.8的区别的内存模型差异? jsk1.7的内存模型: 堆分为初生代和老年代,大小比例为1:2,初生代又分为eden.from.to三个区域,大小比例为8:1:1 方法区:有代码区.常量 ...

  2. Bugku CTF练习题---MISC---宽带信息泄露

    Bugku CTF练习题---MISC---宽带信息泄露 flag:053700357621 解题步骤: 1.观察题目,下载附件 2.下载到电脑里发现是一个bin文件,二进制文件的一个种类,再看名称为 ...

  3. 设置 Visual Studio 总是以管理员身份运行

    话不多说直接上干货 第一步: 打开 Visual Studio 的安装目录,找到 devenv.exe,然后右键快捷菜单选择"兼容性疑难解答". 第二步: 选择故障排查选项 疑难解 ...

  4. FreeRTOS --(8)任务管理之创建任务

    转载自https://blog.csdn.net/zhoutaopower/article/details/107034995 在<FreeRTOS --(7)任务管理之入门篇>文章基本分 ...

  5. lab_0 清华大学ucore实验环境配置详细步骤!(小白入)

    实验步骤 1.下载项目 从github上 的https://github.com/kiukotsu/ucore下载 ucore lab实验: git clone https://github.com/ ...

  6. Ping原理详解

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 前言 Ping是排除设备访问故障的常见方法.它使用Internet控制消息协议ICMP(Int ...

  7. python3修改HTMLTestRunner,生成有截图的测试报告,并发送测试邮件(二)

    3. 如何将第一步得到的地址和名称 输入 进第二步里的表格中呢... 用上述查找元素的方法,发现HTMLTestRunner.py中REPORT_TEST_WITH_OUTPUT_TMPL是用来输出测 ...

  8. ELK 1.4 logstash各种插件

      kibana各种插件: 1.过虑插件 kv (1)KV插件:接收一个键值数据,按照指定分隔符解析为Logstash 事件中的数据结构,放到事件顶层.  常用字段:    • field_split ...

  9. RapidIO 逻辑层IO操作与Message操作的原理和区别

    接上一篇 SRIO RapidIO (SRIO)协议介绍(一) 1     说明 查看协议手册时会发现,逻辑层的操作分成了IO和Message 2类动作,那么为什么要分成2类操作?从原理和应用角度来看 ...

  10. TornadoFx设置保存功能((config和preference使用))

    原文地址:TornadoFx设置保存功能(config和preference使用) 相信大部分的桌面软件都是存在一个设置的界面,允许用户进行设置的修改,此修改之后需要保存的本地,若是让开发者自己实现, ...