AQS详解

 

🎈 简介

是一个锁框架,各大显示锁和 JUC 里面一些工具都是集成 AQS 实现的

全称 AbstractQueuedSynchronizer -->简称同步锁框架

  • 用到了模板模式 tryacquire()realse() 等方法需要子类覆写 / 抛了 sup 异常的都是要子类覆写的

  • 一般的 lock 类都是实现 Lock 接口的,而内部会继承 AQS,因为 Lock 是面向使用者的,而 AQS 这个框架是锁的实现框架,使用者一般不会关心 AQS

  • 主要用于解决锁分配给“谁”的问题,整体就是一个抽象的 FIFO 队列来完成资源获取线程的排队工作,并通过一个 int 类变量表示持有锁的 zhua

🎈 CLH 队列(FIFO)

  • CLH 锁其实就是一种基于队列(具体为单向链表)排队的自旋锁, 由于是 Craig、Landin 和 Hagersten 三人一起发明的,因此被命名为 CLH 锁,也叫 CLH 队列锁

  • 简单的 CLH 锁可以基于单向链表实现,申请加锁的线程首先会通过 CAS 操作在单向链表的尾部增加一个节点,之后该线程只需要在其前驱节点上进行普通自旋,等待前驱节点释放锁即可。

  • 由于 CLH 锁只有在节点入队时进行一下 CAS 的操作,在节点加入队列之后,抢锁线程不需要 进行 CAS 自旋,只需普通自旋即可。因此,在争用激烈的场景下,CLH 锁能大大减少 CAS 操作的数量,以避免 CPU 的总线风暴

  • 声明一个 node 节点,locked 是自身,myPred 是前一个 node 节点的引用(组成一个单向链表),因为获取锁的线程,可能是多个,后节点会不断的自旋(普通自旋)自己的 myPred 指向的前一个 node 的 Locked 属性,当这个属性探测得到是 false 的时候,说明前驱节点已经释放了锁,自己应该去抢锁了

🎈 State

使用 volatile 修饰的,表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。

  • 初始值=0 表示没有线程抢占,1=锁已经被抢占(抢占之后调用 unLock()方法会释放锁,将 state=1 重置为 0) / 大于 1 的情况,说明是可重入锁,举例子:该值等于 3,说明已经被重入了 3 次,最后递减 state 的时候也应该递减 3 次,重置为 0 的时候表示已经释放了锁

  • getState-获取 state 状态

  • setState-设置 state 状态

  • compareAndSetState-乐观锁机制设置 state 状态

  • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

🎈 Node 节点

  • Thread : 当前线程

  • Prev : 上个节点

  • Next : 下个节点

  • Head node : 头节点,指向队列第一个节点

  • Tail node : 尾节点,指向队列最后一个节点

  • predecessor()方法 : 返回上一个节点

🎈 AQS 结构

🎈 AQS 内部结构

🎈 AQS 的执行流程

CAS 尝试获取锁,失败进入 acquire

双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点是从第二个节点开始的

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

1、tryAcquire(尝试抢占)

tryAcquire 尝试获取锁,主要有四种情况

  • 锁已经被其他线程获取;

  • 锁没有被其他线程获取,但是需要排队;

  • cas 失败(可能过程中其他线程获取到锁);

  • 获取到锁;

可以明显的看出公平锁和非公平锁本质区别就是多了一个限制条件hasQueuedPredecessors()

主要判定当前线程前面是否有其他线程正在排队,有则表示当前线程需要排队

流程:

  1. 先获取 state,判断是否=0,判断是否需要排队,尝试 CAS,成功设置持有锁的线程 id 为当前线程

  2. 判断当前线程是否持有锁线程(可重入)

2、addWaiter(入队)

将当前线程封装成 Node 对象,并加入排队队列中


/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
private Node addWaiter(Node mode) {
    //将当前线程封装成Node对象
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //获取队列
    Node pred = tail;
    //队列不等于null 已经初始化了
    if (pred != null) {
        //将新的node加入排队队列末尾即可
        node.prev = pred;
        if (compareAndSetTail(pred, 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
            //CAS初始化一个空的Node,作为哨兵节点
            if (compareAndSetHead(new Node()))
                //作为排队队列的head
                tail = head;
        } else {
            //当前节点的prev指向哨兵节点 
            node.prev = t;
            //CAS将当前节点设置成队列的尾节点
            if (compareAndSetTail(t, node)) {
                //哨兵节点的next指向当前节点
                t.next = node;
                return t;
            }
        }
    }
}

3、acquireQueued(唤醒、通知稳定在队列中)

final boolean acquireQueued(final Node node, int arg) {
    //标识节点可能取消排队
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //获取当前节点的前辈节点
            final Node p = node.predecessor();
            /**
             * 判断前辈节点是否是head,如果是,说明它是下一个可以获得锁的线程则调用一次
             * tryAcquire,尝试获取锁,若获取到,则将链表的关系重新维护
             */
            if (p == head && tryAcquire(arg)) {
                //设置头节点为当前节点
                setHead(node);
                //将前辈节点置为null,从队列中移除
                p.next = null; // help GC
                //正常结束,不需要取消排队
                failed = false;
                return interrupted;
            }
            /**
             * 如果前辈节点不是head,或者获取锁失败,在判断其前辈节点的waitStatus,是不是SIGNAL
             * 如果是则当前线程调用park,进入阻塞状态。
             * 如果不是需要再次循环上面的逻辑在执行一遍
             */
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前辈节点的waitStatus
    int ws = pred.waitStatus;
    //如果waitStatus等于-1 准备状态,等着唤醒,直接返回
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    //表示前辈节点已经被移除
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            //取消前辈节点,从列表移除,重新维护列表关系
            // 将前任的前任 赋值给 当前的前任
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
         // 将前任的前任的 next 赋值为 当前节点
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
         //CAS将前辈节点的waitStatus设置成-1
         //希望自己的上一个节点在释放锁的时候,通知自己(让自己获取锁)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}


private final boolean parkAndCheckInterrupt() {
    //线程挂起,不会在继续执行
    LockSupport.park(this);
    //再次调用unpark继续执行
    return Thread.interrupted();
}

private void setHead(Node node) {
    //当前节点设置为头结点
    head = node;
    //将当前节点线程设置null
    node.thread = null;
    //将当前节点的prev设置null
    node.prev = null;
}

4、release

public final boolean release(int arg) {
    //此处会返回true,如果重入锁会返回false
    if (tryRelease(arg)) {
        //获取头结点
        Node h = head;
        // 所有的节点在将自己挂起之前,都会将前置节点设置成 SIGNAL,希望前置节点释放的时候,唤醒自己。
        // 如果前置节点是 0 ,说明前置节点已经释放过了。不能重复释放了,后面将会看到释放后会将 ws 修改成0.
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

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;
        //将持有锁的线程id设置null
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

//此时传入的节点是头结点
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
     
    int ws = node.waitStatus;
    if (ws < 0)
        // 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
     
    // 如果 next 是 null,或者 next 被取消了。就从 tail 开始向上找节点。
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        /*
         * 从尾部开始,向前寻找未被取消的节点,直到这个节点是 null,或者是 head。
         * 也就是说,如果 head 的 next 是 null,那么就从尾部开始寻找,直到不是 null 为止,找到这个 head 就不管了。
         * 如果是 head 的 next 不是 null,但是被取消了,那这个节点也会被略过。
         */
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
     /*
     * 唤醒 head.next 这个节点。
     * 通常这个节点是 head 的 next。
     * 但如果 head.next 被取消了,就会从尾部开始找。
     */
    if (s != null)
        LockSupport.unpark(s.thread);
}

🎈 LockSupport

1、作用

  1. 阻塞指定线程: park()

  2. 唤醒指定线程: unpark()

  3. 构建同步组件的基础工具

❗️注意点: 方法基本是 Unsafe 类实现的,native 本地方法,因为线程相关操作大部分都需要通过操作系统的配合才能完成,而 Java 并不能直接和操作系统打交道,只能通过 native 本地的 c++方法去打交道,而 sun 公司提供了一个类 Unsafe,里面可以通过调用 native 方法去间接和 OS 打交道,相当于 unsafe 类就是 java 平台的一个后门;

2、常见的方式

  • 先 park(),再 unpark(),线程会阻塞,unpark 后继续执行。

  • 先 unpark(),再 park(),线程不会阻塞,park 后仍然可以继续执行。

3、实现原理

  • 这两个方法的底层都是 native 的,我们看不见实现过程,所以下面直接归纳整理出原理及设计到的变量,理解就好。

  • 在底层实现中,每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex。

  • 简单描述下:_counter 就是是否阻塞的标记 , _cond 就好比阻塞后线程的容器, _mutex 是互斥锁,线程需持有后进入_cond 中。

4、park()流程

  1. 执行 park()方法

  2. 检查_counter 是否是 0

  3. 如果_counter 是 0,则获取互斥锁_mutex

  4. 获取到互斥锁,进入_cond 中进行阻塞

  5. 再次设置_counter 为 0

5、unpark()流程

  1. 执行 unpark(thread-1),设置_counter=1

  2. 唤醒_cond 中阻塞的线程 thread-1

  3. 线程 thread-1 恢复运行

  4. 再次设置_counter = 0

🎈 AQS 底层在唤醒等待线程的时候要从队尾往前遍历

可以从源码看出,unpark 的线程保存在后继节点中,通常是下一个节点。但是如果被取消或明显为 null,则从 tail 向后遍历以找到实际未取消的后继节点

首先需要知道 AQS 中,线程入队时的指针变动操作。

  1. 将线程包装为 node,将该节点 prev 前指针指向 tail 节点。

  2. 将 tail 标记指向新创建的节点。(入队)

  3. 将旧的 tail 节点 next 指针指向新的节点。

至此完成新增节点入队闭环。

极端并发的情况下,如果从前向后查找需要唤醒的线程节点时,有新节点入队且只进行到 2 步骤,那么此时前后遍历只能索引到倒数第二个节点,然后出现断层,漏掉了新节点。

如果从后往前查找,可以完整遍历完所有节点。

🎈 进入队列前还尝试去做一次 CAS 抢锁

抢不到锁的节点, 进入等待队列前, 会判断自己前一个节点是否是头节点, 如果是, 说明自己是第一个进队列的人, 就尝试 CAS 抢锁

因为并发编程是其他线程也在同步并行, 在这个过程中, 前面拿到锁的人, 很可能已经释放过锁, 我这个时候尝试 CAS 抢锁, 成功性非常大, 充分考虑每个非原子性的操作中间别的线程在做什么, 我能做什么优化空间 , 因为并发编程, 并不是时时刻刻在往死里并发 就算你用户量很大, 其实线程基本是交替执行的, 这种小动作, 做的贡献是最有效的 , 这是早期的 sync 不关心的, 所以 AQS 的开创性意义比较大, 引发了后面 sync 的锁优化升级迭代

🎈 AQS 为什么用双向链表,(为啥不用单向链表)

因为 AQS 中,存在取消节点的操作,如果使用双向链表只需要两步

  • 需要将 prev 节点的 next 指针,指向 next 节点。

  • 需要将 next 节点的 prev 指针,指向 prev 节点。

但是如果是单向链表,需要遍历整个单向链表才能完成的上述的操作。比较浪费资源。

🎈 为什么要创建一个虚拟节点呢

事情要从 Node 类的 waitStatus 变量说起,简称 ws。每个节点都有一个 ws 变量,用于这个节点状态的一些标志。初始状态是 0。如果被取消了,节点就是 1,那么他就会被 AQS 清理。还有一个重要的状态:SIGNAL —— -1,表示:当当前节点释放锁的时候,需要唤醒下一个节点。所有,每个节点在休眠前,都需要将前置节点的 ws 设置成 SIGNAL。否则自己永远无法被唤醒。

❓为什么需要这么一个 ws :

防止重复操作。假设,当一个节点已经被释放了,而此时另一个线程不知道,再次释放。这时候就错误了。所以,需要一个变量来保证这个节点的状态。而且修改这个节点,必须通过 CAS 操作保证线程安全。

❓为什么要创建一个虚拟节点呢?

每个节点都必须设置前置节点的 ws 状态为 SIGNAL,所以必须要一个前置节点,而这个前置节点,实际上就是当前持有锁的节点。那第一个节点怎么办?他是没有前置节点的。那就创建一个假的

总结:

每个节点都需要设置前置节点的 ws 状态(这个状态为是为了保证数据一致性),而第一个节点是没有前置节点的,所以需要创建一个虚拟节点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值