AQS简述

AQS是一个FIFO的双向队列,队列元素类型为Node(也就是Thread)。AQS有一个state属性,ReentrantLock可以用来便是当前线程获取锁的可重入次数;对于samaphore来说,state表示当前可用信号的个数;对于CountDownLatch,state表示计数器当前的值。

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

对于AQS来说,线程同步的关键是对状态值state操作。操作state的方式有独占和共享。使用独占方式获取的资源与线程绑定,一个线程获取了资源,会标记这个线程获取到了,其他线程想获取,发现获取资源的不是自己,就会陷入阻塞状态。

共享方式的资源与具体线程是不相关的,比如Semaphore信号量,当一个线程通过acquire获取信号量时,会看信号量个数是否满足,不满足就把线程放入阻塞队列,如果满足通过CAS获取信号量。

AQS的内部队列

AQS是JUC提供的一个用于构建锁和同步容器的基础类。例如ReentrantLock、Semaphore、CountDownLatch、FutureTask等都是基于AQS构建的。AQS解决了实现同步容器时设计的大量细节问题。

AQS是CLH队列的一个变种。AQS队列内部维护的是一个FIFO的双向链表,每个节点有前驱结点和后继节点。每个节点由线程封装,当线程争抢锁失败后会封装成节点加入AQS队列中;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

AQS的核心成员

AQS出于“分离变与不变”(单一职责和开闭原则)的原则,基于模版模式实现。AQS为锁获取、锁释放的排队和出队过程提供了一系列的模版方法。由于JUC的显式锁种类丰富,因此AQS将不同锁的具体操作抽取为钩子方法,让各种锁的子类去实现。

状态标志位

AQS中维持了一个单一的volatile变量state,state表示锁的状态。

private volatile int state;

state保证了可见性,所以任何线程通过getState()获取状态都可以得到最新值。AQS提供了compareAndSetState()方法利用底层UnSafe的CAS机制来实现原子性。

protected final boolean compareAndSetState(int exepect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

以ReentrantLock为例,state初始化为0,表示未锁定。A线程执行该锁的lock()操作时,会调用tryAcquire独占该锁并将state加1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取锁的(state会累加),这就是可重入。但是,获取多少次就要释放多少次,这样才能保证state回到零态。

AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个基类只有一个变量exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了get和set方法。

队列节点类

Node

FIFO双向同步队列

每当线程通过AQS获取锁失败时,线程将被封装成一个Node节点,通过CAS原子操作插入队列尾部。当有线程释放锁时,AQS会尝试让队头的后继节点占用锁。

JUC显式锁与AQS的关系

AQS是一个同步器,它实现了锁的基本抽象功能,该类是由模版模式来实现的。

1.ReentrantLock与AQS的组合关系

ReentrantLock是一个可重入的互斥锁,可以被单个线程多次获取。ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:

static abstract class Sync extends AbstractQueuedSynchronizer { ... }

ReentrantLock支持公平锁和非公平锁。默认情况下是非公平锁。

final static class NonfairSync extends Sync { ... }
final static class FairSync extends Sync { ... }

由ReentrantLock的lock和unlock的源码可以看到,它们只是分别调用了sync对象的lock和release方法。

public void lock() {
sync.acquire(1);
} public void unlock() {
sync.release(1);
}

而Sync内部类只是AQS的子类,所以本质是ReentrantLock的操作是委托给AQS完成的。

AQS的模版流程

AQS定义了两种资源共享方式:

  • Exclusive(独享锁):只有一个线程能占有锁资源,如ReentrantLock。
  • share(共享锁):多个线程可以同时占有资源,如Semaphore、CountDownLatch。

AQS为不同的资源共享方式提供了不同的模版流程,AQS提供了一种实现阻塞锁和依赖FIFO等待队列的同步器的框架。自定义的同步器只需要实现共享资源state的获取与释放方式即可,这些逻辑都编写在钩子方法中。无论是共享锁还是独占锁,AQS在执行模版流程时都会回调自定义的钩子方法。

AQS的钩子方法

自定义同步器时,AQS中需要重写的钩子方法如下:

  • tryAcquire(int):独占锁钩子,尝试获取资源,若成功则返回true,若失败则返回false。
  • tryRekease(int):独占锁钩子,尝试释放资源,若成功则返回true,若失败则返回false。
  • tryAcquireShared(int):共享锁钩子,尝试获取资源,负数表示失败;
  • isHeldExclusively():独占锁钩子,判断该线程是否正在独占资源。只有用到condition条件队列时才需要去实现它。

通过AQS实现简单的独占锁

SimpleMockLock只实现了Lock接口的两个方法:

(1)lock方法:完成显式锁的抢占。

(2)unlock方法:完成显式锁的释放。

SimpleMockLock的锁抢占和释放是委托给Sync实例的方法来实现的。在抢占锁时,AQS的acquire会调用tryAcquire钩子方法;释放锁时,AQS的release会调用tryRelease钩子方法。

内部类Sync继承AQS类时提供了一下两个钩子方法的实现:

(1)tryAcquire:将state设置为1并保存当前线程,表示互斥锁已经占用。

(2)tryRelease:将state设置为0,表示互斥锁已经被释放。

public class SimpleMockLock implements Lock {

    // 同步器实例
private final Sync sync = new Sync(); // 自定义的内部类:同步器
// 直接使用 state 表示锁的状态
// state = 0 表示锁没有被占用
// state = 1 表示已经被占用
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
} @Override
protected boolean tryRelease(int arg) {
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
// 接下来不需要使用CAS操作,因为下面的操作不存在并发场景
setExclusiveOwnerThread(null);
setState(0);
return true;
}
} @Override
public void lock() {
sync.acquire(1);
} @Override
public void lockInterruptibly() throws InterruptedException { } @Override
public boolean tryLock() {
return false;
} @Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
} @Override
public void unlock() {
sync.release(1);
} @Override
public Condition newCondition() {
return null;
} static int i = 0; public static void lockAndFastIncrease(Lock lock) {
lock.lock();
i++;
System.out.println(i);
lock.unlock();
} public static void main(String[] args) {
LongAdder cnt = new LongAdder();
final int TURNS = 1000;
final int THREADS = 10;
final ExecutorService pool = Executors.newFixedThreadPool(THREADS);
final SimpleMockLock lock = new SimpleMockLock();
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS; i++) {
pool.submit(() -> {
try {
for (int j = 0; j < TURNS; j++) {
lockAndFastIncrease(lock);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
final long l = System.currentTimeMillis() - start;
System.out.println("耗时:" + l);
pool.shutdown();
} }

AQS锁抢占原理

流程的第一步,显式锁的lock方法会调用同步器基类AQS的模版方法acquire。acquire是AQS封装好的获取资源的公共入口,它是AQS提供的利用独占的方式获取资源的方法,源码如下

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

acquire至少执行一次tryAcquire钩子方法,tryAcquire默认抛出一个异常,具体的获取独占资源state的逻辑需要钩子方法来实现。若调用tryAcquire尝试成功,则acquire将直接返回,表示抢到锁;若不成功,则将线程加入等待队列中。

tryAcquire流程:CAS操作state字段,将值从0改为1,若成功表示锁未被占用,返回true;若失败,则返回false。如果是重入锁,state字段值会累积,表示重入次数。

直接入队:addWaiter。在acquire模版方法中,如果钩子方法tryAcquire返回失败,就构造同步节点(独占式节点模式为Node.EXCLUSIVE),通过addWaiter方法将节点加入同步队列的队尾。

自选入队:enq。addWaiter第一次尝试在尾部添加节点失败,意味有并发抢锁发生,需要自旋。enq方法通过CAS自旋将节点添加到队列尾部。

自旋抢占:acquireQueued。节点入队之后,启动自旋锁的流程,acquireQueued的主要逻辑:当前Node节点线程在死循环中不断获取同步状态,并且在前驱结点上自旋,只有当前驱结点是头结点时才尝试获取锁。为了不浪费资源,如果头结点获取了锁,那么该节点会终止自旋,线程回去执行临界区的代码。其余处于自旋状态的线程当然也不会自旋浪费资源,而是被挂起进入阻塞状态。

Re

《Java高并发核心编程》

和《Java并发编程之美》结合一起看比较好。

抽象同步队列AQS原理和实践的更多相关文章

  1. Java 同步锁ReentrantLock与抽象同步队列AQS

    AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它.AQS继承了AbstractOwnableSynchronizer类, ...

  2. Java并发编程3-抽象同步队列AQS详解

    AQS是AtractQueuedSynchronizer(队列同步器)的简写,是用来构建锁或其他同步组件的基础框架.主要通过一个int类型的state来表示同步状态,内部有一个FIFO的同步队列来实现 ...

  3. 并发之AQS原理(二) CLH队列与Node解析

    并发之AQS原理(二) CLH队列与Node解析 1.CLH队列与Node节点 就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队. 一条队列是队列中每一个人 ...

  4. Java并发编程(您不知道的线程池操作), 最受欢迎的 8 位 Java 大师,Java并发包中的同步队列SynchronousQueue实现原理

    Java_并发编程培训 java并发程序设计教程 JUC Exchanger 一.概述 Exchanger 可以在对中对元素进行配对和交换的线程的同步点.每个线程将条目上的某个方法呈现给 exchan ...

  5. AQS 原理以及 AQS 同步组件总结

    1 AQS 简单介绍 AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面. AQS 是一个用来构建锁和同步 ...

  6. 【死磕Java并发】-----J.U.C之AQS:CLH同步队列

    此篇博客全部源代码均来自JDK 1.8 在上篇博客[死磕Java并发]-–J.U.C之AQS:AQS简单介绍中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个F ...

  7. 5. AQS(AbstractQueuedSynchronizer)抽象的队列式的同步器

    5.1 AbstractQueuedSynchronizer里面的设计模式--模板模式 模板模式:父类定义好了算法的框架,第一步做什么第二步做什么,同时把某些步骤的实现延迟到子类去实现. 5.1.1 ...

  8. AQS独占式同步队列入队与出队

    入队 Node AQS同步队列和等待队列共用同一种节点结构Node,与同步队列相关的属性如下. prev 前驱结点 next 后继节点 thread 入队的线程 入队节点的状态 INITIAl 0 初 ...

  9. J.U.C之AQS:CLH同步队列

    此篇博客所有源码均来自JDK 1.8 在上篇博客[死磕Java并发]—–J.U.C之AQS:AQS简介中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个FIFO ...

  10. 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)

    前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ...

随机推荐

  1. C# 俩个字符串数据进行对比取出不同的数据

    string sel_JD = "电冰箱,电热水壶,客厅空调,饮水机,"; string Operation_JD = "电冰箱,电热水壶,电动汽车,"; Li ...

  2. 七牛云服务器debug

    400{ResponseInfo:com.qiniu.http.Response@16fd3806,status:400, reqId:uzcAADGFlzHUE-kW, xlog:-, xvia:, ...

  3. centos7的mysql主从

    互为主从关闭两台虚拟机的防火墙1.yum -y install mariadb-server mariadb2.Systemctl start mariadb3.进入配置文件:vi /etc/my.c ...

  4. iOS包大小计算

    一.LinkMap文件分析 说明:LinkMap数据是根据文章<LinkMap文件分析>中方法实验实测数据. 如何获得LinkMap文件 1.在XCode中开启编译选项Write Link ...

  5. Flutter 新建项目

    一.打开VSCode,点击侧边栏扩展按钮,搜索flutter,安装Flutter和Dart 二.按command+shift+P,输入flutter,选择Flutter:New project,新建项 ...

  6. ??????FormDataAccess

    using Genersoft.Fms.Center.Pub.Com;using Genersoft.Platform.AdpBizIntegration.Entity;using Genersoft ...

  7. SQL servr——基础篇之DML增删改查

    DML:数据操作语言 用于添加.更新.删除和查询数据库中的语言 添加--insert语句 向数据表中插入新的行(记录)可多行可一行 语法:insert  [into]  表名  [(column1,. ...

  8. nvm在windows下安装与使用

    1.卸载本地已经安装的所有node 2.nvm下载 下载地址https://github.com/coreybutler/nvm-windows ,选择nvm-noinstall.zip 放在本地盘, ...

  9. 运行不出来真的QAQ

    学C的时候最大的苦恼是编译不通过和运行不正确 学了C++之后就开始有编译过了但运行不出来的情况了TAT

  10. Java 中的内存分配

    Java 中的内存分配 Java 程序运行时,需要在内存中分配空间.为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式. 一.栈:储存局部变量 局部变量 ...