查看完整视频
小黑屋思过中,禁止观看!
评论并刷新后可见

您需要在视频最下面评论并刷新后,方可查看完整视频

积分观看

您支付积分,方可查看完整视频

{{user.role.value}}
付费视频

您支付费用,方可查看完整视频

¥{{user.role.value}}
课程视频

架构师VIP免费观看课程视频


开始学习
会员专享

视频选集

AQS源码深度剖析

  • 课程笔记
  • 问答交流

在并发编程领域,AQS号称是并发同步组件的基石,很多并发同步组件都是基于AQS实现,所以想掌握好高并发编程,你需要掌握好AQS。

本篇主要通过对AQS的实现原理、数据模型、资源共享方式、获取锁的过程,让你对AQS的整体设计有清晰了解,让你迈出高并发编程的第一步。

为了助大家掌握好AQS,本节课我会重点讲解以下8点:

1.AQS的核心概念

2.AQS资源共享方式

3.AQS核心成员

4.AQS数据模型

5.AQS实现原理

6.AQS锁核心源码剖析

7.AQS锁的获取原理与流程图

8.AQS的总结

AQS

AQS(AbstractQueuedSynchronizer)就是一个抽象的队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它。

AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,比如大家熟知的:

  • ReentrantLock
  • Semaphore
  • CountDownLatch
  • CyclicBarrier

等并发类均是基于AQS来实现的。

AQS的核心概念

AQS(AbstractQueuedSynchronizer)就是一个抽象的队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它 AQS是一个Java提供的底层同步工具类,用一个volatile int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,比如:

  • ReentrantLock(重入锁)
  • Semaphore(信号量)
  • CountDownLatch(倒计时)

同步状态

AQS中维持一个全局的volatile int state来表示同步状态,线程通过修改(加/减指定的数量)码是否成功来决定当前线程是否成功获取到同步状态。

CLH同步队列

维护了一个FIFO双向队列,该队列就是CLH同步队列,来完成线程等待排队。

CLH同步队列是AQS的核心,用来完成同步状态的管理,当线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造成一个节点并加入到同步队列,同时会阻塞当前线程。

备注:CLH(Craig, Landin, andHagersten)根据三个人的名字命名的队列

AQS资源共享方式

AQS支持两种获取同步状态的模式既独占式和共享式。
AQS源码深度剖析-mikechen的互联网架构师之路

1.独占锁

顾名思义,独占式模式同一时刻只允许一个线程获取同步状态。

典型代表:ReentrantLock采用独占模式

ReentrantLock还可以分为公平锁和非公平锁:

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

2.共享锁

共享模式则允许多个线程同时获取。
典型代表:Semaphore、CountDownLatch、CyclicBarrier等

AQS的数据模型

AQS源码深度剖析-mikechen的互联网架构师之路

AQS 使用上图的资源变量 state来表示同步状态,通过内置的 CLH FIFO 队列来完成获取资源线程的排队工作,这里会涉及到三个要素:

1.AQS的三个核心成员变量

  • 共享资源:volatile int state(代表共享状态)
  • 队头节点:head头节点
  • 队尾节点:tail尾节点

head、tail、state三个变量都是volatile的,通过volatile来保证共享变量的可见性。

2.AQS中state状态的变更是基于CAS实现的
主要有三种方法:

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

state状态通过volatile保证共享变量的可见性,再由CAS 对该同步状态进行原子操作,从而保证原子性和可见性。

3.CLH队列(FIFO队列)

CLH队列通过内置的FIFO队列(Node来实现),来完成线程等待排队 (多线程争用资源被阻塞时会进入此队列)。

CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

AQS的锁获取与释放原理

1.线程获取锁流程:

  • 线程A获取锁,state将0置为1,线程A占用
  • 在A没有释放锁期间,线程B也来获取锁,线程B获取state为1,表示线程被占用,线程B创建Node节点放入队尾(tail),并且阻塞线程B
  • 同理线程C获取state为1,表示线程被占用,线程C创建Node节点,放入队尾,且阻塞线程

2.线程释放锁流程:

  • 线程A执行完,将state从1置为0
  • 唤醒下一个Node B线程节点,然后再删除线程A节点
  • 线程B占用,获取state状态位,执行完后唤醒下一个节点 Node C,再删除线程B节点

更加详细的锁获取和释放过程,建议通过查看源码的方式学习AQS独占模式和共享模式下的获取锁过程。

独占模式获取与释放状态

多线程获取锁,修改同步状态

/**
* 获取同步状态
*/
public final void acquire(int arg) {
    /**
     * 1. tryAcquire    尝试获取同步状态;
     * 2.1 addWaiter     如果尝试获取到同步状态失败,则加入到同步队列中;
     * 2.2 acquireQueued 在队列中尝试获取同步状态.
     */
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

 

1.获取锁

/**
* 尝试获取同步状态【子类中实现】,因为aqs基于模板模式,仅提供基于状态和同步队列的实
* 现框架,具体的实现逻辑由子类决定
*/
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // a. 尝试修改状态值操作执行成功
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // b. 修改状态值成功,记录当前持有同步状态的线程信息
            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;
}

 

2.获取锁失败,加入队列尾部

/**
* 没有获取到同步状态的线程加入到队尾部
*/
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)) {
            pred.next = node;
            return node;
        }
    }
    // 正常的入队方法
    enq(node);
    return node;
}

3.同步队列中节点,尝试获取同步状态

/**
* 同步队列中节点,尝试获取同步状态
*/
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;
            }
            // 获取失败后是否进入wait
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

 

AQS独占锁获取流程图总结

交流专区
  1. 李鸿翼

    AQS是抽象队列同步器,是java并发的底层基石。
    AQS的核心模型包含2个: 一个volatile的state变量,一个fifo的队列。state状态的改变是通过cas操作来执行,保障了操作的原子性,而volatile修饰符保障了多线程间的state状态的可见性。
    reentrantlock是基于AQS的独占锁实现,countdownlatch、cyclebarrier等是基于aqs的共享锁实现。
    获取锁的流程图如下:
    (ps:老师,自旋那里,我看了代码,自旋第二次,就阻塞了,不是一直自旋)

    • mikechen

      是的,你这个更加准确,自旋这里会发生两次,两次后会进入阻塞状态:

      第一次:前驱的状态为0,执行compareAndSetWaitStatus后返回false。

      第二次:才能进入if (ws == Node.SIGNAL)这个分支,才能返回true。

      如果当前线程一直不能获取到锁,死循环保证了最终一定能设置前驱为SIGNAL成功。

      这里关于SIGNAL与阻塞的理解,可以用闹钟与睡觉这个比喻更容易来理解:

      比如:前驱节点已经设置了SIGNAL,表示闹钟已经设好,可以安心睡觉(阻塞)。

  2. 李鸿翼

    补充图

  3. 路正银

    AQS的底层实现:
    一个全局的volitile int state来表示同步状态,用volatile来保证可见性,用CAS来保证原子性
    CLH同步队列来完成线程等待排队
    基于AQS的同步组件有:
    ReentrantLock(独占锁)
    Semaphore、CountDownLatch、CyclicBarrier(共享锁)

  4. 路正银

    图:

    • mikechen

      手画练习马上就来了 ✗咧嘴笑✗

  5. JansenZhang

    AbstractQueuedSynchronizer,抽象队列同步器,是jdk为同步器提供的一个框架。
    在AQS中,主要有三个变量,head,tail,state,这三个变量均使用volatile关键字进行修饰,保证了多线程情况下的可见性。
    AQS的使用state来标记同步状态,在使用过程中设置state是通过CAS操作来实现,从而保证了原子性。AQS使用CLH(FIFO)队列来存储线程的等待节点,当线程获取同步状态失败时,会将当前线程和等待状态等信息构造成一个节点加入到同步队列的队尾,同时会阻塞当前线程。
    ReentrantLock、Semaphore、CountDownLatch等都是通过AQS方式实现。

  6. JansenZhang

    图片上传不知道怎么用

    • mikechen

      图片可以结合文字正常提交,我这边刚看到了,没有问题。

    • JansenZhang

      嗯,这个图片上传了以后在文本框里没有回显,所以不能及时知道有没有上传成功。现在知道怎么使用了。

搜索