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. PHPUnit简介及使用(thinkphp5的单元测试安装及使用)

    PHPUnit简介及使用(thinkphp5的单元测试安装及使用) 一.总结 一句话总结:直接google这个phpunit(how to use phpunit),然后去官网看使用样例和手册,那些英 ...

  2. 不能访问虚拟机上的nginx网站

    VMware虚拟机上配置nginx后,本机无法访问问题 转自:http://www.server110.com/nginx/201407/10794.html 把nginx装在CentOS上,用本机访 ...

  3. python numpy 学习

    例子 >>> import numpy as np >>> a = np.arange(15).reshape(3, 5) >>> a array ...

  4. gitlab访问限制问题------Forbidden

    解决方案: cd /etc/gitlab vim /gitlab.rb gitlab_rails['rack_attack_git_basic_auth'] = { 'enabled' => t ...

  5. flask学习(十三):过滤器

    1. 介绍和语法 介绍:过滤器可以处理变量,把原始的变量经过处理后再展示出来,作用的对象是变量

  6. SpringMVC中的参数绑定总结

    众所周知,springmvc是用来处理页面的一些请求,然后将数据再通过视图返回给用户的,前面的几篇博文中使用的都是静态数据,为了能快速入门springmvc,在这一篇博文中,我将总结一下springm ...

  7. oracle会自动收集统计信息-记住哦

    oracle自动收集统计信息,周一至周五  时间:22:00:00 oracle自动收集统计信息,周六.周日  时间:06:00:00

  8. 表单验证jq.validate.js

    源代码--demo Validate:function(){ var me=this; var $form = $('#form'); //添加自定义方法: 同时验证手机和座机电话    jQuery ...

  9. 折叠纸片PFold.js

    PFold.js是一款折叠纸片插件,支持定义折叠纸牌数量.折叠动画效果.折叠方向,而且还支持折叠结束后回调方法. 在线实例 效果一 效果二 效果三 使用方法 <div id="uc-c ...

  10. 1月中旬值得一读的10本技术新书(机器学习、Java、大数据等)!

    1月中旬,阿里云云栖社区 联合 博文视点 为大家带来十本技术书籍(机器学习.Java.大数据等).以下为书籍详情,文末还有福利哦! 书籍名称:Oracle数据库问题解决方案和故障排除手册 内容简介 & ...