抽象同步队列AQS原理和实践
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原理和实践的更多相关文章
- Java 同步锁ReentrantLock与抽象同步队列AQS
		AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它.AQS继承了AbstractOwnableSynchronizer类, ... 
- Java并发编程3-抽象同步队列AQS详解
		AQS是AtractQueuedSynchronizer(队列同步器)的简写,是用来构建锁或其他同步组件的基础框架.主要通过一个int类型的state来表示同步状态,内部有一个FIFO的同步队列来实现 ... 
- 并发之AQS原理(二) CLH队列与Node解析
		并发之AQS原理(二) CLH队列与Node解析 1.CLH队列与Node节点 就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队. 一条队列是队列中每一个人 ... 
- Java并发编程(您不知道的线程池操作), 最受欢迎的 8 位 Java 大师,Java并发包中的同步队列SynchronousQueue实现原理
		Java_并发编程培训 java并发程序设计教程 JUC Exchanger 一.概述 Exchanger 可以在对中对元素进行配对和交换的线程的同步点.每个线程将条目上的某个方法呈现给 exchan ... 
- AQS 原理以及 AQS 同步组件总结
		1 AQS 简单介绍 AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面. AQS 是一个用来构建锁和同步 ... 
- 【死磕Java并发】-----J.U.C之AQS:CLH同步队列
		此篇博客全部源代码均来自JDK 1.8 在上篇博客[死磕Java并发]-–J.U.C之AQS:AQS简单介绍中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个F ... 
- 5. AQS(AbstractQueuedSynchronizer)抽象的队列式的同步器
		5.1 AbstractQueuedSynchronizer里面的设计模式--模板模式 模板模式:父类定义好了算法的框架,第一步做什么第二步做什么,同时把某些步骤的实现延迟到子类去实现. 5.1.1 ... 
- AQS独占式同步队列入队与出队
		入队 Node AQS同步队列和等待队列共用同一种节点结构Node,与同步队列相关的属性如下. prev 前驱结点 next 后继节点 thread 入队的线程 入队节点的状态 INITIAl 0 初 ... 
- J.U.C之AQS:CLH同步队列
		此篇博客所有源码均来自JDK 1.8 在上篇博客[死磕Java并发]—–J.U.C之AQS:AQS简介中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个FIFO ... 
- 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)
		前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ... 
随机推荐
- 群晖Video Station不支持部分视频的解释
			网络上都是替换ffmpeg插件的做法,无非就是替换了3个文件,然后再对其中一个文件进行修改. 然而在DSM7.0.1+VS3.0.2中,这个方法根本无用,最好的结果是之前无法播放的视频播放起来转圈圈而 ... 
- vc++生成随机数
			在VC++提供的函数为rand(),返回一个0至65535之间的随机数,若想产生0至MAX_NUM之间的随机数,可用rand()%MAX_NUM,即产生小于MAX_NUM的随机数 for (int i ... 
- uniapp+vue3+ts
			1. 创建vue3的默认uniapp模板 2. npm init 创建package.json 
- C#——》创建Windows服务,发布并调试Windows服务
			一,创建一个windows服务项目. 二,双击Service1.cs进入设计界面,在空白处右键单击选择添加安装程序,如下图所示. 三,添加安装程序后,会进入如下图界面,生成两个组件:serviceP ... 
- win10 系统 腾讯云服务器 部署网站 并进行访问
			1.首先需要一个服务器 我是用的Windows系统 我用的是腾讯云的服务器(因为便宜) 然后给服务器重置密码 然后用重置后的密码 用户名 用远程桌面连接登录试试 远程桌面连接成功 然后回到服务器网站 ... 
- 20220718 第七组 陈美娜  java
			如果把变量直接声明在类里:成员变量(全局变量)成员变量->属性 如果把变量声明在某个方法里:局部变量 public:访问权限修饰符,后面讲 void:没有返回值 run():方法名,标识符 {} ... 
- UI自动化之【chromedriver.exe无法删除问题】
			想删掉chromedriver.exe,结果提示被打开 在任务管理器中,找到Chromedriver.exe,结束进程 
- 云服务器搭建redis主从复制以及哨兵模式(附踩坑记录)
			云服务器搭建redis主从复制以及哨兵模式(附踩坑记录) 踩坑记录见最后 搭建一主两从: 在根目录下任意新建一个目录/myredis来存放配置文件: 将我们常用的redis.conf文件拷贝到该目录下 ... 
- 如何安装vm虚拟机软件并用该软件建立虚拟机
			一.安装vm虚拟机软件 1.双击打开虚拟机应用程序 找到VM应用程序所在的位置,双击安装 2.根据向导安装 根据提示点击下一步 点击安装之后耐心等待一会,会出现需要输入许可证的的界面,这时候不要关闭界 ... 
- CI框架 between and sql语句
			1.在文档里没有找到关于where() between and 相应的说明 每次组合查询 要么写原生 要么连续调用where方法. 可用以下方式组合 $condition = array(); $co ... 
