【参考文章】

https://www.jianshu.com/p/df0d7d6571de

http://ifeve.com/introduce-abstractqueuedsynchronizer/

【 AbstractQueuedSynchronizer概述 】

AbstractQueuedSynchronizer,队列同步器,简称AQS,基于FIFO,是Java并发用来构建锁或者其他同步组件的基础框架。

一般使用AQS的主要方式是继承,子类通过实现它提供的抽象方法来管理同步状态(同步器用了一个int值来表示状态),主要管理的方式是通过tryAcquire()和tryRelease()类似的方法来操作状态,同时AQS提供以下线程安全的方式来对状态进行操作:

protected final int getState();
protected final void setState(int newState);
protected final boolean compareAndSetState(int expect, int update);
【AQS主要是怎么使用的呢?】
在Java的同步组件中,AQS的子类一般是同步组件的静态内部类。

【AQS和同步组件的关系】

AQS是实现同步组件的关键,它两的关系:

同步组件是面向使用者的,它定义了使用者与组件交互的接口,隐藏了具体的实现细节。

AQS面向的是同步组件的实现类,它简化了具体的实现方式,屏蔽了线程切换相关的底层操作。

它俩一起很好地对使用者和是实现者锁关注的领域做了一个隔离。

【AQS实现分析】

【同步队列分析】

AQS的实现以来内部的同步队列(FIFO双向队列)来完成同步状态的管理,如果当前线程获取同步状态(锁)失败,AQS会将该线程以及等待状态能信息构造成一个Node,如下:

Node node = new Node(Thread.currentThread(), mode);

并将其加入同步队列,同时阻塞当前线程。当同步状态释放时候,唤醒队列的首节点。

【Node】

Node主要包含以下成员变量

static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}

[ Node成员变量—— waitStatus ]

Node是AbstractQueuedSynchronizer中的一个静态内部类。

表示节点的状态

CANCELLED 1,当前线程被取消
SIGNAL-1,当前节点的后继节点包含的线程需要运行,即unpark
CONDITION-2,当前节点在等待condition,即在condition队列中
PROPAGETE-3,当前场景下后续的acquireShared能够可以执行
:当前节点在sync队列中,等待着获取锁

[ Node成员变量 —— prev ]

前驱节点,如果当前节点被取消,那就需要前驱节点和后继节点完成连接。

[ Node成员变量 —— next ]

后继节点。

[ Node成员变量 —— thread ]

入队时的当前线程。

[ Node成员变量 —— nextWaiter ]

存储condition队列中的后继节点。

Node节点成为sync队列和condition队列构建的基础。在AQS中就拥有了三个成员变量。

【 AQS成员变量 】

对于锁的获取,请求形成节点,将其挂载在尾部,而锁资源的转移(释放再获取)是从头部开始遍历向尾部进行的。

[ AQS 插入节点 ]

节点插入,AQS提供基于CAS的设置尾部节点的方法,如下:

参数:需要传递当前线程认为的尾节点和当前节点,设置成功后,当前节点和尾节点建立关系。

[ AQS 删除节点 ]

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

注意:设置首节点是由获取同步状态成功的线程来完成,因为每次只有一个线程能获取到同步状态,所以,设置首节点并不需要CAS来保证。

【AQS的API说明】

方法名称 描述
protected boolean tryAcquire(int arg) 排它地获取这个状态。这个方法的实现需要查询当前的同步状态是否允许获取,然后再进行获取(使用compareAndSetState来做)状态。
protected boolean tryRelease(int arg)  释放状态。
protected int tryAcquireShared(int arg) 共享的模式下获取同步状态。
protected boolean tryReleaseShared(int arg) 共享的模式下释放同步状态。
protected boolean isHeldExclusively() 在排它模式下,同步状态是否被占用。

【 AQS源码分析——acquire( int arg )方法 】

[执行流程]

1.调用tryAcquire(arg)方法尝试获取同步状态(锁)。

2.如果获取不到同步状态,将当前线程构造成Node节点并加入同步队列。

3.再次尝试获取,如果还是没有获取到,那么将当前线程从调度器上摘下,进入等待状态。

[ acquire( int arg )-->addWaiter(Node node) 方法]

1.使用当前的Thread构造Node。

2.尝试在队尾插入节点,如果节点已经存在,就做以下操作:

- 分配引用T指向尾节点;
- 将待插入节点的prev指针指向尾节点;
- 如果尾节点还为T,将当前尾节点设置为带待插入节点;
- T的next指针指向待插入节点。

3.快速在队尾插入节点,失败则进入enq(Node node)方法。

[ acquire( int arg ) --> addWaiter(Node node) --> enq(Node node) 方法]

enq(Node node)的逻辑可以确保Node可以有顺序地添加到同步队列中,具体的加入逻辑如下:

1.初始化同步队列,如果尾节点为空,分配一个头结点,并将尾节点指向头结点。

2.节点入队,通过CAS将节点设置为尾节点,以此在队尾做节点插入。

可以看出,整个enq(Node node)方法通过“死循环”来保证节点的正确插入。

进入同步队列之后,接下来就是同步状态的获取,或者说是访问控制acquireQueued。对于同步队列中的线程,在同一时刻只能由队列首节点获取同步状态,其他的线程进入等待,只到符合条件才能继续进行。

[ acquire( int arg )--> acquireQueued(Node node , int arg)方法 ]

1.获取当前节点的前驱节点

2.如果当前节点的前驱节点是头节点,并且可以获取同步状态(锁),设置当前节点为头结点,该节点占有锁。

3.不满足条件的线程进入等待状态。

整个方法中,当前线程一直在“死循环”中获取同步状态。

节点自旋获取同步状态,如下图:

从代码的逻辑也可以看出,其实节点与节点之间在循环检查的过程中,是不会相互通信的,仅仅只是判断自己当前的前驱节点是不是头结点,这样的设计使得节点的释放符合FIFO,同时也避免了过早通知。

注意:过早通知是指 前驱节点不是头结点的线程由于终端而被唤醒。

[ acquiure()实现总结 ]

1.同步状态维护

对同步状态的操作是原子、非阻塞的,通过AQS提供的对状态访问的方法来对同步状态进行操作,并且利用CAS来确保原子操作。

2.状态获取

一旦线程成功地修改了同步状态,那么该线程会被设置为同步队列的头结点。

3.同步队列维护

不符合同步状态的线程会进入等待状态,直到符合条件被唤醒再开始执行。

30_AQS的更多相关文章

随机推荐

  1. zero-copy总结

    基本概念 零拷贝,通常在java NIO编程中会使用,比如netty网络工具包. 其真实意思是: 网卡或者其他外设进行io操作时不经过CPU, 而是直接和主memory交互,不经过CPU寄存器,这样可 ...

  2. vue学习(转载)

    vue.js库的基本使用 第一步:下载 官网地址:https://cn.vuejs.org/v2/guide/installation.html 第二步:放到项目的文件目录下 一般新建一个js文件夹, ...

  3. lua路径问题

    方法1:lua进行require绝对路径时,会从package.path中进行遍历 print(package.path)会得到类似下面的结果: --> "lualibs/p4ulib ...

  4. 自动化测试接口PYTHON

      在开发测试中经常会遇到接口迭代和代码重构,一个无关紧要的改动往往会引起整个项目的运行.现有的接口测试中往往只是针对单一接口的测试,可是业务的连贯性是非常紧密的,比如:用户从登陆,获取商品信息,下单 ...

  5. sort sorted() reverse() reversed() 的区别1

    sort()是可变对象(字典.列表)的方法,无参数,无返回值,sort()会改变可变对象,因此无需返回值.sort()方法是可变对象独有的方法或者属性,而作为不可变对象如元组.字符串是不具有这些方法的 ...

  6. 《大数据日知录》读书笔记-ch1数据分片与路由

    目前主流大数据存储使用横向扩展(scale out)而非传统数据库纵向扩展(scale up)的方式.因此涉及数据分片.数据路由(routing).数据一致性问题 二级映射关系:key-partiti ...

  7. 差分ADC到单端ADC

    单片机可以处理单端ADC(不在电压范围内要进行分压),也可以处理差分ADC(但需要双路输入).差分信号在传输过程中抗共模干扰能力很强,所以传输中都用差分传输,到ADC时可以差分也可以单端(需要放大器处 ...

  8. Ubuntu 16.04安装SecureCRT替代XShell

    XShell应该是最强大的,在Ubuntu下只有SecureCRT能实现跨平台(Linux/Windows/Mac),并且可以实现Tab的功能等.当然,还有其它的类似PuTTY这些.Windows下建 ...

  9. Oracle 数据库、表、方案的逻辑备份与恢复

    数据库(表)的逻辑备份与恢复 逻辑备份是指使用工具export将数据对象的结构和数据导出到文件的过程,逻辑恢复是指当数据库对象被破坏而使用工具import利用备份的文件把数据对象导入到数据库的过程,逻 ...

  10. LinuxShell脚本编程7-for和while

    1.for的使用 #! /bin/bash ` do echo $a done 表示:a初始值为1,然后a=a+2的操作,一直到a<=10为止 for((i=1;i<=10;i=i+2)) ...