J.U.C学习的第二篇AQS。AQS在Java并发包中的重要性,毋庸置疑,所以单独拿出来理一理。本文参考总结自《Java并发编程的艺术》第五章第二节队列同步器。

什么是AbstractQueuedSynchronizer?

AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchronizer是抽象同步队列,简称AQS,是实现同步器的基础组件,并发包中锁的实现底层就是使用AQS实现,当然大多数人不会直接用到AQS,但是学习这个类对并发包的底层理解还是有莫大的帮助的。

AQS中维持了一个单一的状态信息state,可以通过getState,setState,compareAndSetState 函数修改其值,AQS内部维持一个FIFO队列来完成资源获取线程的排队工作。对于ReentrantLock 的实现来说,state 可以用来表示当前线程获取锁的可重入次数;

对应读写锁ReentrantReadWriteLock 来说state 的高16位表示读状态,也就是获取该读锁的次数,低 16位 表示获取到写锁的线程的可重入次数;对于FuterTask 来说,state用来表示任务状态(例如,还没开始,运行,完成,取消);

对应CountDownlatch 和CyclicBarrie 来说,state用来表示计数器当前的值。

AQS有个内部类ConditionObject 是用来结合锁实现线程同步,ConditionObject可以直接访问AQS对象内部的变量,比如 state 状态值 和AQS队列;

ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),用来存放调用条件变量的await()方法后被阻塞的线程。

对于AQS 来说,线程同步的关键是对状态值state进行操作,根据state是否属于一个线程,操作state的方式分为独占模式和共享模式。

独占模式下获取和释放资源使用方法的源码如下:

void acquire(int arg)
void acquireInterruptibly(int arg)
boolean release(int arg)

共享模式下获取和释放资源方法的源码如下:

void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
boolean releaseShared(int arg)

另外还有个查询同步队列等待线程情况的方法如下:

Collection<Thread> getQueuedThreads()

对于独占锁方式获取的资源是与具体线程绑定的,也就是说如果一个线程获取到了资源,就会标记是那个线程获取到的,其他线程尝试操作state获取资源时候发现当前该资源不是自己持有,就会获取失败后被阻塞;

比如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock的锁后,AQS内部会首先使用CAS操作把state状态从0 变成 1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁的时候,发现当前线程就是锁的持有者,则会把state状态值从1变成2,

也就是设置可重入次数,当另外一个线程获取锁的时候发现自己并不是该锁的持有者就会被放入AQS阻塞队列后挂起。

对于共享操作方式资源是与具体线程不相关的,多个线程去请求资源时候是通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次获取时候,如果 当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可,

共享模式下并不需要记录哪个线程获取了资源;比如 Semaphore 信号量,当一个线程通过acquire()方法获取一个信号量时候,会首先看当前信号两个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量。

队列同步器的实现分析

1、同步队列

 同步队列即当线程获取同步状态失败,同步器会将当前线程以及等待信息构成节点Node加入同步队列,同时阻塞当前线程,当同步状态释放,会把首节点的线程唤醒,使其再次尝试获取同步状态。

节点是构成同步队列的基础,同步器拥有首节点head和尾节点tail,获取状态失败的线程会加入队列的尾部,结构如图:

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,后继节点将会在获取同步状态成功时将自己设置为首节点,如下图

2、独占式同步状态获取与释放

调用acquire(int arg)方法获取同步状态,当获取失败进入同步队列,后续线程中断,也不会从同步队列移出。

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

首先我们调用acquire(int arg)方法后,然后调用自定义同步器实现的tryAcquire方法 尝试获取同步状态,具体是设置状态变量state的值,成功则直接返回。失败则将当前线程封装为类型Node.EXCLUSIVE 的Node节点,并调用addWaiter(Node node)方法将该节点插入到AQS阻塞队列尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态,如果获取不到则阻塞节点线程,而被阻塞的线程的唤醒主要依靠前驱节点的出队或被中断来实现。

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

上述尾节点的添加代码可知,使用了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);
}
}

当前线程以“死循环”的方式获取同步状态,而只有前驱节点是头节点才能尝试获取同步状态,这又是为什么呢,原因有两点。

第一,头节点是成功获取到同步状态的节点,而头节点释放状态后,将唤醒后继节点,后继节点被唤醒后也需要检查自己的前驱节点是否是头节点。

第二,维护同步队列的FIFO原则。

独占式的获取同步状态的流程如下:

当一个线程获取同步状态并执行完相关逻辑后,就需要释放同步状态,使得后续节点能够获取,具体代码如下:

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

该方法会在释放同步状态后,唤醒后继节点,unparkSuccessor(Node node)方法使用LockSupport来唤醒等待的线程。

总结:在获取同步状态时,同步器维护了一个同步队列,获取状态失败的线程都会被加入到队列并在队列自旋;移除队列的条件是前驱节点为头节点且成功获取同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

这里需要注意的是AQS类并没有提供可用的tryAcquire 和 tryRelease,正如AQS是锁阻塞和同步容器的基础框架,是抽象类,tryAcquire和 tryRelease 需要有具体的子类来实现的。

子类在实现tryAcquire 和 tryRelease 时候要根据具体场景使用CAS算法尝试修改该状态值state,成功则返回true,否则返回false。子类还需要定义在调用acquire 和 release 方法时候 state 状态值的增减代表什么含义。

比如继承自AQS实现的独占锁ReentrantLock,定义当status为0的时候标示锁空闲,为1 的时候标示锁已经被占用,在重写tryAcquire的时候,内部需要使用CAS算法看当前status是否为0,如果为0 则使用CAS设置为1,

并设置当前线程持有者为当前线程,并返回true,如果CAS失败则返回false。继承自 AQS 实现的独占锁实现 tryRelease 时候,内部需要使用CAS算法把当前status值从1 修改为0,并设置当前锁的持有者为null,然后返回true,如果CAS失败则返回false。

3、共享式同步状态获取与释放

共享模式和独占模式最大的区别就是同一时刻是否能有多个线程同时获取状态。

当共享模式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞。而独占式访问资源时,同一时刻其他任何访问均被阻塞。

  public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

线程调用acquireShared(int arg) 获取共享资源时候,会首先使用tryAcquireShared尝试获取资源,当返回值大于等于0,表示可以获取状态。失败则将当前线程封装为类型Node.SHARED 的 Node 节点后插入到 AQS 阻塞队列尾部,并使用 LockSupport.park(this) 挂起当前线程。

在doAcquireShared(arg)方法中,如果前驱节点为头节点,尝试获取同步状态大于0,则表示可以获取同步状态并从自旋过程中退出。

public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

当一个线程调用 releaseShared(int arg) 时候会尝试使用, tryReleaseShared 操作释放资源,这里是设置状态变量 state 的值,然后使用 LockSupport.unpark(thread)激活 AQS 队列里面最早被阻塞的线程 (thread)。

同理需要注意的 AQS 类并没有提供可用的 tryAcquireShared 和 tryReleaseShared,正如 AQS 是锁阻塞和同步器的基础框架,tryAcquireShared 和 tryReleaseShared 需要有具体的子类来实现。

子类在实现 tryAcquireShared 和 tryReleaseShared 时候要根据具体场景使用 CAS 算法尝试修改状态值 state, 成功则返回 true,否者返回 false。

比如继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryAcquireShared 时候,首先看写锁是否被其它线程持有,如果是则直接返回 false,否者使用 CAS 递增 status 的高 16 位,在 ReentrantReadWriteLock 中 status 的高 16 为获取读锁的次数。

继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryReleaseShared 时候,内部需要使用 CAS 算法把当前 status 值的高 16 位减一,然后返回 true, 如果 cas 失败则返回 false。

并发包学习(三)-AbstractQueuedSynchronizer总结的更多相关文章

  1. HTTP学习三:HTTPS

    HTTP学习三:HTTPS 1 HTTP安全问题 HTTP1.0/1.1在网络中是明文传输的,因此会被黑客进行攻击. 1.1 窃取数据 因为HTTP1.0/1.1是明文的,黑客很容易获得用户的重要数据 ...

  2. TweenMax动画库学习(三)

    目录               TweenMax动画库学习(一)            TweenMax动画库学习(二)            TweenMax动画库学习(三)           ...

  3. Struts2框架学习(三) 数据处理

    Struts2框架学习(三) 数据处理 Struts2框架框架使用OGNL语言和值栈技术实现数据的流转处理. 值栈就相当于一个容器,用来存放数据,而OGNL是一种快速查询数据的语言. 值栈:Value ...

  4. 4.机器学习——统计学习三要素与最大似然估计、最大后验概率估计及L1、L2正则化

    1.前言 之前我一直对于“最大似然估计”犯迷糊,今天在看了陶轻松.忆臻.nebulaf91等人的博客以及李航老师的<统计学习方法>后,豁然开朗,于是在此记下一些心得体会. “最大似然估计” ...

  5. DjangoRestFramework学习三之认证组件、权限组件、频率组件、url注册器、响应器、分页组件

    DjangoRestFramework学习三之认证组件.权限组件.频率组件.url注册器.响应器.分页组件   本节目录 一 认证组件 二 权限组件 三 频率组件 四 URL注册器 五 响应器 六 分 ...

  6. [ZZ] 深度学习三巨头之一来清华演讲了,你只需要知道这7点

    深度学习三巨头之一来清华演讲了,你只需要知道这7点 http://wemedia.ifeng.com/10939074/wemedia.shtml Yann LeCun还提到了一项FAIR开发的,用于 ...

  7. SVG 学习<三>渐变

    目录 SVG 学习<一>基础图形及线段 SVG 学习<二>进阶 SVG世界,视野,视窗 stroke属性 svg分组 SVG 学习<三>渐变 SVG 学习<四 ...

  8. Android JNI学习(三)——Java与Native相互调用

    本系列文章如下: Android JNI(一)——NDK与JNI基础 Android JNI学习(二)——实战JNI之“hello world” Android JNI学习(三)——Java与Nati ...

  9. day91 DjangoRestFramework学习三之认证组件、权限组件、频率组件、url注册器、响应器、分页组件

    DjangoRestFramework学习三之认证组件.权限组件.频率组件.url注册器.响应器.分页组件   本节目录 一 认证组件 二 权限组件 三 频率组件 四 URL注册器 五 响应器 六 分 ...

  10. Django基础学习三_路由系统

    今天主要来学习一下Django的路由系统,视频中只学了一些皮毛,但是也做下总结,主要分为静态路由.动态路由.二级路由 一.先来看下静态路由 1.需要在project中的urls文件中做配置,然后将匹配 ...

随机推荐

  1. Android:你好,androidX!再见,android.support

    1.AndroidX简介 点击查看Android文档中对androidx的简介 按照官方文档说明 androidx 是对 android.support.xxx 包的整理后产物.由于之前的suppor ...

  2. JavaScript返回上一页和返回上一级页面并刷新

    JavaScript返回上一页和刷新当前页 window.history.go(-1); //返回上一页 window.history.back(); //返回上一页 //如果要强行刷新的话就是:wi ...

  3. 2-15-MySQL进阶

    select select 字段列表 from 数据表 [[as] 别名] [where 条件] 别名: 数据表 [[as] 别名] select AA.money,BB.name from prod ...

  4. 基于Open XML 导出数据到Excel

    数据导出的结果: 步骤1.新建一个Excel 文档,模板根据自己需要设置 步骤2.使用OpenXml  打开Excel 文件 步骤3.点击ReflectCode 功能,生成相应的代码文档 using ...

  5. ES6学习一 JS语言增强篇

    一 背景 JavaScript经过二十来年年的发展,由最初简单的交互脚本语言,发展到今天的富客户端交互,后端服务器处理,跨平台(Native),以及小程序等等的应用.JS的角色越来越重要,处理场景越来 ...

  6. java基础第5天

    数组概述 数组是储存多个变量(元素)的东西(容器} 这多个变量的数据类型要一致 概念:数组是存储同一种数据类型多个元素的集合.也就是一个容器,这个容器有个名字,就是数组名. 数组就是在内存中开辟出一段 ...

  7. oracle创建定时器详解|interval属性

    定时任务首先先创建定时任务中的存储过程 create or replace procedure pro_jggl as                                          ...

  8. 《次元唤醒 需求规格说明书v1.0》

    一.团队分工 组员 工作比例 参与范围 王诚荣 17% 原型设计,需求规格说明书整合,LOGO设计 马祎特 22% PPT制作,演讲,博客模板,用户描述 陈斌 21% 评审表格制作,引言,项目描述,功 ...

  9. HDU 4635

    http://acm.hdu.edu.cn/showproblem.php?pid=4635 问:最多加多少条边,使得原图不是强连通图 正向考虑有困难,不妨反向思考,既最少去掉几条边使得原图不是强连通 ...

  10. PAT 1012 数字分类 C语言

    给定一系列正整数,请按要求对数字进行分类,并输出以下5个数字: A1 = 能被5整除的数字中所有偶数的和: A2 = 将被5除后余1的数字按给出顺序进行交错求和,即计算n1-n2+n3-n4...: ...