AQS是并发编程的一个最基本组件,是一个抽象同步器。

网上有很多详细介绍AQS的博文,在这里我就不仔细介绍了,主要写一些重要的内容。

AQS中重要的几个属性:

//同步队列的头节点
private transient volatile Node head;
//同步队列的尾节点
private transient volatile Node tail;
//同步状态
private volatile int state;

由于一个共享资源同一时间可以被一条线程持有,也可以被多个线程持有,因此AQS中存在两种模式,共享模式独占模式

  • 共享模式是共享状态值state每次可以由多个线程持有,如CountDownLatchSemaphore
  • 独占模式是共享状态值state每次只能由一条线程持有,其他线程如果需要获取,则需要阻塞。如ReentrantLock

独占锁的获取

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

先尝试获取锁,获取失败会调用addWaiter将当前线程添加到同步队列,之后队列中的每个节点会调用acquireQueued()方法通过自旋的方式先再尝试获取一下锁,如果失败,将当前节点的前驱节点的状态设置为SIGNAL,并将该线程阻塞,并判断该线程是否被中断。如果被中断了,当前节点获取锁后进行中断操作。

这里用到了模版方法的设计模式,tryAcquire是一个抽象方法,具体实现需要到子类中去完成。

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

下面详细介绍获取独占锁失败后,添加到队列的过程,调用addWaiter()方法。

private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

先创建出一个节点

  1. 当前尾节点不为空,采用尾插法利用CAS机制将新创建的节点添加到尾部,设置为尾节点。如果添加失败,就执行enq()方法。
  2. 如果尾节点为空,调用enq()方法。
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;
}
}
}
}

enq是一个自旋操作,

1) 如果尾节点为空,说明当前线程是第一个加入同步队列的,先用CAS操作新添一个头节点head,并将尾节点指向它。第二次循环会跳往另一个执行区域。

2) 利用CAS操作将该节点添加到尾部。

直到自旋添加成功,就结束循环。

入队成功后,就要为该节点开启自旋,尝试获得锁。

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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

先获取当前节点的先驱节点

1) 如果先驱接节点是头节点并且成功获取同步状态的时候,将当前节点设置为头节点,然后将之前的头节点的next指针设置为null并且pre指针也为null,即将前节点与队列断开,

2)如果获取失败,就调用shouldParkAfterFailedAcquire方法,主要作用是将该节点的前驱节点的状态设置为SIGNAL,表示线程阻塞。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

shouldParkAfterFailedAcquire方法中,如果前驱节点的状态是SIGNAL,就直接返回true;如果状态值大于0,表明该前驱节点已经被取消,则将该前驱节点去除掉,向前移;其他情况,将该前驱节点设置为SIGNAL。如果添加失败,就返回false,因为是自旋,下一次再尝试。

由于acquireQueued方法是一个循环,在第二次执行到shouldParkAfterFailedAcquire方法时,由于0号节点的waitStatus已经为Node.SIGNAL了,所以shouldParkAfterFailedAcquire方法会返回true,然后继续执行parkAndCheckInterrupt方法,将该线程已经阻塞,并怕判断该线程是否中断。

private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

独占锁的释放

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

独占锁通过tryRelease释放成功后,如果头节点head不为null,并且状态值不为0,就会对它的后继节点进行唤醒。

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)
//后继节点不为null时唤醒该线程
LockSupport.unpark(s.thread);
}

如果头节点的后继节点是空或者它的状态值大于0,表明它是失效的,就要从尾节点节点向前查找,找到最后一个状态值小于等于0的节点,然后对该节点进行唤醒。

共享锁的获取与释放

public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

共享锁可以同时被多个线程拥有,可以在初始设置的时候将state设置为大于0的值,每一个线程获取一次,就减1,当state大于等于0时,别的线程也能够拥有该锁,当小于0时,就不可以,在共享锁模式下,当前线程拿到锁后,会直接通知后继节点去拿锁,而不必等待锁被释放的时候再通知。 在锁释放的时候,支持多个线程释放同步线程同步状态。

参考文章:

深入理解AbstractQueuedSynchronizer(AQS)

java并发编程系列:牛逼的AQS(上)

AQS源码分析总结的更多相关文章

  1. ReentrantLock 与 AQS 源码分析

    ReentrantLock 与 AQS 源码分析 1. 基本结构    重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized ...

  2. 并发-AQS源码分析

    AQS源码分析 参考: http://www.cnblogs.com/waterystone/p/4920797.html https://blog.csdn.net/fjse51/article/d ...

  3. AQS源码分析笔记

    经过昨晚的培训.对AQS源码的理解有所加强,现在写个小笔记记录一下 同样,还是先写个测试代码,debug走一遍流程, 然后再总结一番即可. 测试代码 import java.util.concurre ...

  4. AQS源码分析看这一篇就够了

      好了,我们来开始今天的内容,首先我们来看下AQS是什么,全称是 AbstractQueuedSynchronizer 翻译过来就是[抽象队列同步]对吧.通过名字我们也能看出这是个抽象类 而且里面定 ...

  5. JAVA AQS源码分析

    转自:  http://www.cnblogs.com/pfan8/p/5010526.html JAVA AQS的全称为(AbstractQueuedSynchronizer),用于JAVA多线程的 ...

  6. AbstractQueuedSynchronizer AQS源码分析

    申明:jdk版本为1.8 AbstractQueuedSynchronizer是jdk中实现锁的一个抽象类,有排他和共享两种模式. 我们这里先看排他模式,共享模式后面结合java.util.concu ...

  7. AQS源码分析

    AQS全程为AbstractQueuedSynchronizer,其定义了一套多线程访问共享资源的同步框架,大部分的同步类的实现都依赖于他,比如ReentrantLock,ReentrantReadW ...

  8. java中AQS源码分析

    AQS内部采用CLH队列.CLH队列是由节点组成.内部的Node节点包含的状态有 static final int CANCELLED =  1; static final int SIGNAL    ...

  9. ArrayList源码分析--jdk1.8

    ArrayList概述   1. ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合.  2. ArrayList支持随机访问.克隆.序列化,元素有序且可以重复.  3. ...

随机推荐

  1. C语言寒假大作战03

    这个作业属于哪个课程 软件4班 这个作业要求在哪里 C语言寒假大作战03 这个作业的目标 增加菜单程序各年级题目操作函数 参考文献 随机数rand 2.2.2 设计思路和遇到的问题 这次作业写好没多久 ...

  2. js代码预解析

    1.var一般用于声明变量,预解析代码的时候,等号后面的赋值过程不会执行,所以预解析时的var变量都是未定义的 2.function声明的函数,预解析的时候,值就是函数里面的内容 例:console. ...

  3. virtualenv 指定 python 解释器的版本

    使用如下命令为 ubuntu 系统安装 virtualenv sudo apt-get install python-virtualenv 当我们使用 virtualenv 命令创建虚拟环境时,默认使 ...

  4. HDU_3183_RMQ

    http://acm.hdu.edu.cn/submit.php?pid=3183 初探rmq,这道题看了题解还是写了好久.原因是rmq处理字符串时没有自己写min函数,导致把返回的字符当成下标处理了 ...

  5. 计算机网络 & 网络编程 期末总结与测评题

      第一部分:网络编程部分的相关知识 Socket套接字定义: 套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开.读写和关闭等操作.套接字允许应用程序 ...

  6. socket实现文件上传(客户端向服务器端上传照片示例)

    本示例在对socket有了基本了解之后,可以实现基本的文件上传.首先先介绍一下目录结构,server_data文件夹是用来存放客户端上传的文件,client_data是模拟客户端文件夹(目的是为了测试 ...

  7. java jdk1.8 32/64位 官方绿色版下载附安装教程

    java jdk1.8 32/64位 官方绿色版下载附安装教程 地址:https://www.jb51.net/softs/691593.html

  8. Go语言实现:【剑指offer】栈的压入、弹出序列

    该题目来源于牛客网<剑指offer>专题. 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序.假设压入栈的所有数字均不相等.例如序列1,2,3,4,5 ...

  9. elasticsearch kibana 安装 配置

    二.Elasticsearch  配置信息 2.1  因为 Elasticsearch 可以执行脚本文件,为了安全性,默认不允许通过 root 用户启动服务.我们需要新创建用户名和用户组启动服务 2. ...

  10. CentOS7.3yum安装MariaDB报错[Errno 256]

    在yum安装MariaDB时出现下面这个错误提示,然后着手排查,先检查了/etc/yum.repo.d/下的配置文件,没有错误. failure: repodata/repomd.xml from m ...