1 介绍

AQS: AbstractQueuedSynchronizer,即队列同步器。是构建锁或者其他同步组件的基础框架。它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

state的访问方式有:

  • getState()
  • setState()
  • compareAndSetState()

自定义同步器需要根据需要重写以下方法

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余

    可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false

然后可以调用 acquire、release、releaseShared等方法来实现功能。

AQS定义两种资源共享方式:Exclusive和Share

2 Exclusive独占锁

通过ReentrantLock来分析独占锁。

2.1 lock

ReentrantLock的基本使用形式是:

	ReentrantLock lock = new ReentrantLock();
try {
lock.lock(); // 加锁
} catch (Exception e) { } finally {
lock.unlock(); // 解锁
}

ReentrantLock内部类Sync继承了AQS,ReentrantLock#lock即调用了Sync#lock。Sync又有两个子类分别是NonfairSync和FairSync,分别实现了非公平锁和公平锁。

看下NonfairSync的lock

static final class NonfairSync extends Sync {
// ...
final void lock() {
if (compareAndSetState(0, 1)) // lock的时候直接使用cas去抢占state,成功就返回了,表示抢锁成功
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢占state状态失败才调用AQS的acquire方法
}
// ...
}

再看下FairSync的lock

static final class FairSync extends Sync {
// ...
final void lock() {
acquire(1); // 直接调用AQS的acquire
}
// ...
}

2.2 acquire

acquire是lock调用的关键。自定义锁需要通过acquire来设置state和将节点加入FIFO等待队列操作。

public final void acquire(int arg) {
if (!tryAcquire(arg) && // 用户自定义内容,返回true表示获取锁成功,否则加入等待队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 创建节点,加入等待队列
selfInterrupt(); // if上述true,则中断线程
}

上面代码可以分开来看就会简单点。

  • 首先调用用户自定义的tryAcquire
  • 如果获取锁失败则addWaiter,意思是把当前线程加入等待队列,该方法返回创建的Node
  • acquireQueued会将节点对应的线程,即当前线程park。

2.2.1 tryAcquire

首先看下NonfairSync的tryAcquire

static final class NonfairSync extends Sync {
// ...
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
} final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // state==0表示可以获取锁
if (compareAndSetState(0, acquires)) { // cas设置锁,成功则加锁成功
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果不是0,但是还是当前线程,则可重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 否则加锁失败返回fasle,就要进行加入等待队列的处理
}

再看下FairSync的tryAcquire

static final class FairSync extends Sync {
// ..
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // state==0并且等待队列没有其他线程才会加锁
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

总结来讲,tryAcquire是用户自定义的,根据state设置锁状态,根据返回值来决定是否加入等待队列。公平锁和非公平锁的差异主要在:新来的锁会不会插队。

2.2.2 addWaiter

addWaiter用于初始化队列并增加新的node节点到等待队列中

private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果tail不是null则在tail后加入该节点;设想添加第一个节点的时候,tail为null,则走不到这里,则会调用下面的enq(node)初始化后再加入节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // cas添加失败则调用enq(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 // 如果tail是null,则先初始化
if (compareAndSetHead(new Node())) // 用一个空的node作为head
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

注意:等待队列中的头结点是初始化的空节点或者已经获取到锁的节点,不是正在等待获取锁的节点,即第一个节点是dummy node。

2.2.3 acquireQueued

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)) { // 先尝试获取锁,如果前节点是head并且获取到锁,则当前节点成为head,这也和2.2.2的"注意"相呼应。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 没有获取到锁,判断是否可以park线程,符合条件则当前线程被park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

看下线程被park的条件,即shouldParkAfterFailedAcquire。在这之前我们需要简单了解下Node的waitStatus字段

waitStatus一共四个状态

  • CANCELLED = 1;
  • SIGNAL = -1;
  • CONDITION = -2;
  • PROPAGATE = -3;

这里我们关心两个状态:

  1. CANCELLED,表示当前节点的线程被取消了,也是唯一的正数值。
  2. SIGNAL,表示后续节点需要被唤醒
  3. 另外waitStatus初始化值为0
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 前节点状态为SIGNAL才可以阻塞, 因为初始化值为0,所以第一次是不会直接返回true
return true;
if (ws > 0) { // 前节点取消了,则一直往前遍历,直到找到waitStatus不大于0的
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 设置节点为SIGNAL
}
return false;
}

这里注意到由于waitStatus初始值为0,shouldParkAfterFailedAcquire第一次判断的时候是返回fasle的,即线程不是马上被park,在第二次的时候才会被park。从中也可以看到线程先自旋2次,最后再park:第一次是先尝试获取锁的地方,即if (p == head && tryAcquire(arg))的位置,第二次是因为shouldParkAfterFailedAcquire返回false,所以需要再运行一次。

2.2.4 总结acquire

到现在为止,我们可以看到,没有获取到锁的线程是以节点的形式加入到了等待队列,并且park了,不占用cpu时间。

2.3 unlock

unlock调用了AQS的release方法

public void unlock() {
sync.release(1);
} public final boolean release(int arg) {
if (tryRelease(arg)) { // 用户自定义
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒head节点的后续节点
return true;
}
return false;
}

2.3.1 tryRelease

tryRelease由用户自定义,被AQS中release调用,来看下ReentrantLock中tryRelease的调用

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); // 这里不需要使用cas,因为是独占锁,释放锁的时候,肯定只有一个线程访问
return free;
}

tryRelease简单来说就是设置state,但是注意因为是独占锁,所以并不需要使用cas来设置state。

2.3.2 unparkSuccessor

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

2.4 独占锁总结

到这里可以看到,AQS维护了一个正在获取锁的线程的等待队列,获取到锁的线程是head节点,当释放锁的时候会唤醒其后续节点,通过这样的过程达到了独占锁的效果。并且使用了cas和自旋减少了资源的损耗。

3 Share共享锁

3.1 区别

  • 当独占锁已经被某个线程持有时,其他线程只能等待它被释放后,才能去争锁,并且同一时刻只有一个线程能争锁成功
  • 对于共享锁而言,由于锁是可以被共享的,因此它可以被多个线程同时持有。换句话说,如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,并且极有可能获取成功

其实从代码上来总结,最大的区别就是,共享锁在被唤醒后不但会像独占锁那样将自己的节点设置为head,而且会继续唤醒它的后续节点,后续节点又会唤醒后续节点的节点。这样当一个共享锁获取到锁后,所有等待的线程都将获取到锁。

3.2 CountDownLatch

可以通过分析CountDownLatch来分析下共享锁。CountDownLatch的使用形式可以看成是获取锁和释放锁的过程,这样就更容易理解共享锁了。

CountDownLatch countDownLatch = new CountDownLatch(1);

countDownLatch.await(); // 获取锁,可以是多个线程都在调用

countDownLatch.countDown(); // 释放锁, 当释放后所有获取共享锁的线程都会获取到锁

3.2.1 countDownLatch.await()

countDownLatch.await()可以看做是获取锁。

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 调用AQS的方法
} public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) // 自定义tryAcquireShared
doAcquireSharedInterruptibly(arg); // 和独占锁的tryAcquire基本思想是一致的,如果获取锁失败就加入到等待队列中
}

看下countDownLatch自定义的tryAcquireShared,还是比较简单的

protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1; // state为0则获取锁成功,否则则是获取锁失败
}

然后看下doAcquireSharedInterruptibly

private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED); // addWaiter和独占锁调用的同一个方法,只是节点类型为SHARED
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r); // 和独占锁的主要区别所在,独占锁只是setHead,而共享锁会setHeadAndPropagate,即设置head并且会传播,将后续的共享锁也唤醒
p.next = null; // help GC
failed = false;
return;
}
} // 主要思想和独占锁是一致的
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

看下setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); // 设置当前节点为head // 如果propagate>0才会唤醒后续shared节点,这里propagate为用户自定义的tryAcquireShared的返回值
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); // 唤醒head节点的后续节点, releaseShared同样调用了该方法
}
}

这里主要注意propagate值,即tryAcquireShared的返回值。如果tryAcquireShare<则表示没有获取到锁;如果tryAcquireShare==0则表示获取锁成功,但是不会唤醒后续shared节点,这点从上述代码中可以看到;如果tryAcquireShare>0,则表示获取锁成功且唤醒后续share节点。

3.2.2 countDownLatch.countDown()

countDownLatch.countDown可以看成是释放锁的过程,只不过如果count值不为1的话,需要释放多次才算释放成功。

public void countDown() {
sync.releaseShared(1); // 调用了AQS的releaseShared
} public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); // 和setHeadAndPropagate调用的是同一个方法
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); // 和独占锁一样,唤醒head后续节点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

4 总结

AQS的总体思路是将等待的线程封装成Node节点放在等待队列上。获取锁的节点为head节点,释放锁的时候,head节点会唤醒后续节点关联的线程

需要区别的是:独占锁只会唤醒后续节点的线程;而共享锁后续节点被唤醒后会接着继续唤醒他自己的后续节点,一直到把所有连续的共享节点都唤醒。

5 参考

  1. https://www.cnblogs.com/waterystone/p/4920797.html
  2. https://segmentfault.com/a/1190000016447307

AQS分析笔记的更多相关文章

  1. ReentrantReadWriteLock源码分析笔记

    ReentrantReadWriteLock包含两把锁,一是读锁ReadLock, 此乃共享锁, 一是写锁WriteLock, 此乃排它锁. 这两把锁都是基于AQS来实现的. 下面通过源码来看看Ree ...

  2. 3.View绘制分析笔记之onLayout

    上一篇文章我们了解了View的onMeasure,那么今天我们继续来学习Android View绘制三部曲的第二步,onLayout,布局. ViewRootImpl#performLayout pr ...

  3. 4.View绘制分析笔记之onDraw

    上一篇文章我们了解了View的onLayout,那么今天我们来学习Android View绘制三部曲的最后一步,onDraw,绘制. ViewRootImpl#performDraw private ...

  4. 2.View绘制分析笔记之onMeasure

    今天主要学习记录一下Android View绘制三部曲的第一步,onMeasure,测量. 起源 在Activity中,所有的View都是DecorView的子View,然后DecorView又是被V ...

  5. 1.Android 视图及View绘制分析笔记之setContentView

    自从1983年第一台图形用户界面的个人电脑问世以来,几乎所有的PC操作系统都支持可视化操作,Android也不例外.对于所有Android Developer来说,我们接触最多的控件就是View.通常 ...

  6. zeromq源码分析笔记之线程间收发命令(2)

    在zeromq源码分析笔记之架构说到了zmq的整体架构,可以看到线程间通信包括两类,一类是用于收发命令,告知对象该调用什么方法去做什么事情,命令的结构由command_t结构体确定:另一类是socke ...

  7. glusterfs 4.0.1 api 分析笔记1

    一般来说,我们写个客户端程序大概的样子是这样的: /* glfs_example.c */ // gcc -o glfs_example glfs_example.c -L /usr/lib64/ - ...

  8. SEH分析笔记(X64篇)

    SEH分析笔记(X64篇) v1.0.0 boxcounter 历史: v1.0.0, 2011-11-4:最初版本. [不介意转载,但请注明出处 www.boxcounter.com  附件里有本文 ...

  9. 【转载】Instagram架构分析笔记

    原文地址:http://chengxu.org/p/401.html Instagram 架构分析笔记 全部 技术博客 Instagram团队上个月才迎来第 7 名员工,是的,7个人的团队.作为 iP ...

随机推荐

  1. [旧][Android] 消息处理机制

    备注 原发表于2016.06.06,资料已过时,仅作备份,谨慎参考 概述 Android 的消息处理机制主要是指 Handler 的运行机制以及 Handler 所附带的 MessageQueue 和 ...

  2. Clickhouse 分布式表&本地表

    CK 分布式表和本地表 ck的表分为两种: 分布式表 一个逻辑上的表, 可以理解为数据库中的视图, 一般查询都查询分布式表. 分布式表引擎会将我们的查询请求路由本地表进行查询, 然后进行汇总最终返回给 ...

  3. Vim的强大配置文件(一键配置)

    转:https://blog.csdn.net/u010871058/article/details/54253774/ 花了很长时间整理的,感觉用起来很方便,共享一下. 我的vim配置主要有以下优点 ...

  4. Shell编程四剑客包括:find、sed、grep、awk

    一.Shell编程四剑客之Find Find工具主要用于操作系统文件.目录的查找,其语法参数格式为: find path -option [ -print ] [ -exec -ok command ...

  5. MySQL 学习笔记(一)MySQL 事务的ACID特性

    MySQL事务是什么,它就是一组数据库的操作,是访问数据库的程序单元,事务中可能包含一个或者多个 SQL 语句.这些SQL 语句要么都执行.要么都不执行.我们知道,在MySQL 中,有不同的存储引擎, ...

  6. 禁用所有控制台console.log()打印

    在前端dev的环境下,经常会用到console.log()进行调试,以方便开发, 而在产品release的版本中,又不合适在浏览器的console中输出那么多的调试信息. 但是会经常因为没有删除这些开 ...

  7. 计算机系统4-> 计组与体系结构1 | 基础概念介绍

    在大二上学期学习数字逻辑的过程中,我对计算机如何运作产生了兴趣,因此开了这个系列来记录自己在这方面的学习过程,此前三篇分别是: 计算机系统->Hello World的一生 | 程序如何运行,从大 ...

  8. SpringBoot 搭建基于 MinIO 的高性能存储服务

    1.什么是MinIO MinIO是根据GNU Affero通用公共许可证v3.0发布的高性能对象存储.它与Amazon S3云存储服务兼容.使用MinIO构建用于机器学习,分析和应用程序数据工作负载的 ...

  9. 当我们看到phpinfo时在谈论什么

    我们在渗透测试的过程中,如果存在phpinfo界面,我们会想到什么? 大部分内容摘抄自:https://www.k0rz3n.com/2019/02/12/PHPINFO 中的重要信息/ 关于phpi ...

  10. Linux-本地日志服务管理(rsyslog基础)

    目录 系统环境 1.常见的两种日志管理服务 1.1 RSYSLOG系统日志服务 1.2 ELK 2.RSYSLOG日志服务的相关知识 2.1 RSYSLOG日志消息级别 2.2 RSYSLOG日志服务 ...