前言

AQS(Abstract Queued Synchronizer)是JUC并发包中的核心基础组件,作者是大名鼎鼎的Doug Lea。通过AQS可以实现大部分的同步需求。

宏观架构

AQS包括一个state和一个FIFO的CLH队列,如下图所示:

CLH队列中的每个节点Node就可以对应与争用该资源的线程,Node的数据结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
   static final class  {

        static final Node SHARED = new Node();
/** 独占模式 */
static final Node EXCLUSIVE = null;
/** 当前节点已取消 */
static final int CANCELLED = 1;
/** 当前线程处于同步状态,如果取消或释放,通知下一个等待节点 */
static final int SIGNAL = -1;
/** wait 在某个condition中 */
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
/** 当前节点已取消 */
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
} Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
} Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
``` ### 获取锁的过程
以使用默认构造函数的reentrantLock为例子: ```java
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();

lock()代码如下:

1
2
3
4
5
6
7
final void lock() {
if (compareAndSetState(0, 1))
//成功获得独占的state资源
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

如果当前的state值为0,当前线程获得lock,将state的值通过cas的方式设置为1。如果不是0,则添加到队列中。通过acquire方法去申请资源。

1
2
3
4
5
6
public final void acquire(int arg) {
//tryAcquire再次尝试获取锁
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

tryAcquire:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}

再次获取锁尝试失败后,调用addWaiter将线程封装成节点信息,加入到等待队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { 大专栏  AQS总结
pred.next = node;
return node;
}
}
enq(node);
return node;
}

addWaiter首先会通过cas的方式快速的去添加到队列的尾部,如果添加不成功,调enq(node)再次入队;enq(node)是一个死循环,不断的通过cas去添加到节点,直到成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//cas的方式
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

线程节点进入队列后,调用acquireQueued,acquireQueuedxiang

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}
}

最关键的应该是shouldParkAfterFailedAcquire方法,如果每个线程都在这么自旋的去拿锁,cpu肯定炸了。所以,当当前的前一个节点处于SIGNAL状态的时候,可以挂起当前线程。这个操作就好比在排队的时候和前一个人说:我出去买点吃的,你轮到的时候叫我一下。当前面的节点轮到的时候,会唤醒当前线程,然后又开始自旋,判断自己能否拿到同步状态,如果拿到,就获取到了锁,这就是一个完整的获得同步状态的过程。
至于如何挂起当前线程,使用的是LockSupport的park()挂起当前线程。park可以精确的进行挂起,精确到thread。

释放锁的过程

1
reentrantLock.lock(); //释放锁

释放的过程正好相反,通过release来释放锁。

1
2
3
4
5
6
7
8
9
10
11
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的内容主要是:获取state的值,减去要释放的值,如果state已经是0,把当前的线程设置为null。要注意的是,这里完全没有使用cas,因为当前线程还持有锁,绝对的线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

最为关键的 unparkSuccessor(h),这个时候头节点已经处于了获取同步的状态,通过unparkSuccessor(h)来唤醒头节点的后一个节点。从而后一个节点就可以自旋的去获取同步状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
LockSupport.unpark(s.thread);
}

总结

个人认为AQS在很多地方使用cas和自旋的方式,一定程度上提升吞吐率,之前看到过测试ReentrantLock的吞吐比synchronized要高很多,不对synchronized一直在优化,估计现在性能也差不多了,以后做个测试。本文只是总结了AQS的独占式的获取同步状态,还有共享式的获取同步状态,还支持很多的特性,将在后面进行总结。

AQS总结的更多相关文章

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

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

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

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

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

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

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

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

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

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

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

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

  7. 基于AQS的锁

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

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

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

  9. Java并发包源码学习之AQS框架(三)LockSupport和interrupt

    接着上一篇文章今天我们来介绍下LockSupport和Java中线程的中断(interrupt). 其实除了LockSupport,Java之初就有Object对象的wait和notify方法可以实现 ...

  10. Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁

    上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列 ...

随机推荐

  1. ant design for vue 关于table的一些问题

    1.为table添加分页: :pagination="pagination" pagination: { defaultPageSize: 10, showTotal: (tota ...

  2. springboot访问请求404问题

    新手在刚接触springboot的时候,可能会出现访问请求404的情况,代码没问题,但就是404. 疑问:在十分确定代码没问题的时候,可以看下自己的包是不是出问题了? 原因:SpringBoot 注解 ...

  3. cifar数据集下载

    https://www.cs.toronto.edu/~kriz/cifar.html Version Size md5sum CIFAR-10 python version 163 MB c58f3 ...

  4. Mac下Rblas配置

    Mac 下加速 R做矩阵计算, 感觉比没有R open 快, 但数量级上差不多了. 安装R open时,提示 X11相关的组件缺失,不想再额外安装,怕污染系统文件. cd /Library/Frame ...

  5. day50-线程-定时器

    #1.定时器: from threading import Timer def func(): print('定时器') t = Timer(1,func) #定时一秒,开启func线程. t.sta ...

  6. Java之异常的处理(throws)

    import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java. ...

  7. Introduction to Differential Equations,Exercise 1.1,1.5,1.6,1.8,1.9,1.10

    As noted,if $z=x+iy$,$x,y\in\mathbf{R}$,then $|z|=\sqrt{x^2+y^2}$ is equivalent to $|z|^2=z\overline ...

  8. mysql计算时间差-本例为计算分钟差然后/60计算小时保留一位小数,由于直接得小时只会取整

    -- ORDER_TIME datetime NOT NULL(字段类型)SELECTso.`ID`,so.`ORDER_TIME`,NOW(),CONCAT(ROUND(TIMESTAMPDIFF( ...

  9. 41)PHP,数据库函数的注意点

    (1)那个mysqli_connect()            函数返回的是一个数据库连接对象,所以,你要是var_dump()这个函数的返回值,就是一个对象 (2)那个mysqli_query() ...

  10. 36)PHP,获取数据库数据并在html中显示(晋级3)

    首先展示我的html代码和php文件的位置关系: 然后我的php文件: <?php class db { public $host ;//= "localhost";//定义 ...