AQS的理解与使用

130 阅读4分钟

来源于近期的一次分享,介绍下并发中AQS的实现与使用。

1、AQS组成
  • violate int state的变量,来表示同步状态,通过CAS完成对State值的修改

  • FIFO双端队列(是CLH变体的虚拟双向队列),来完成资源获取的排队

1.1 state介绍

state的值大于1体现可重入性。

state的值来判断是否加锁(以ReentrantLock为例,state为0,没有锁,大于0,视为加锁)

描述
protected final int getState()获取State的值
protected final void setState(int newState)设置State的值
protected final boolean compareAndSetState(int expect, int update)使用CAS方式更新State
1.2 FIFO双端队列

Node节点组成的双向链表

示意图: image.png

在AQS中,没有获取锁的线程,打包到Node节点中。

2 案例分析

这里只是描述了简单的情况,后续会对此案例进行剖析,thread0在获取锁时有可能多次获取,此时state的值是大于1,逐渐修改为0

多个线程共同访问共享资源的情况下,是如何维护同步状态state以及同步队列(FIFO队列)
步骤分析

  • thread 0 获取资源,此时state = 0,此时thread0 修改状态state = 1;thread1加入同步队列时,还会通过CAS尝试获取锁

image.png

  • 此时thread 1 尝试通过CAS方式尝试获取锁,因为锁已经被thread0 持有,此时thread1 进入FIFO同步队列;同样的,thread 2、thread 3、thread 4 加入同步队列

  • 假设thread0需要释放锁,首先会将state的值由1改为0,thread1被唤醒,通过CAS方式获取锁,当thread0获取锁的时候,FIFO会使队头元素出队,

3、从源码中剖析案例问题
3.1、获取同步状态State的两种模式

独占方式和共享方式:指获取state的方式;一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过CAS的方式进行获取。如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中(公平/非公平)。

image.png

3.2、非公平锁体现在哪里(出队)

上述可知,非公平性是指线程不是按照FIFO的方式获取锁,下面从入队和出队来分析非公平性这个过程。

(1) 入队时:在thread1通过CAS获取锁失败后,会加入FIFO同步队列中,加入队列末尾。
线程在获取锁失败时,通过addWaiter()方法被加入到等待队列;
如果无法通过快速路径将线程插入队列尾部,enq()方法会负责将节点放入队列末尾。

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

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // 必要时初始化队列
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

addWaiter()方法中:

  • 线程首先被包装成一个Node对象。
  • 然后尝试将这个新节点通过CAS操作插入到队列的尾部。

如果初次尝试插入尾部失败(例如,由于竞争导致tail节点已更新),则调用enq()方法,正式进入等待队列。

  • enq()方法通过一个自旋循环,确保新节点最终被插入到队列的末尾。
  • 如果队列还未初始化(即tail == null),它会首先初始化队列。
  • 随后,它通过CAS操作不断尝试将当前线程节点插入到队列的末尾,直到成功。

(2)出队时:当thread0锁释放后,此时threa1被唤醒,FIFO通常只唤醒队头元素,但此时如果thread5来获取锁,此时thread1和thread5来竞争,此处体现非公平性。

虽然唤醒了队列头部的线程,但新来的线程依然可以在队列头部线程获取锁之前直接竞争锁。这意味着即使队列头部的线程被唤醒,它也不一定能马上获取锁,而新来的线程有机会“插队”获取锁。

参考文章:
从ReentrantLock的实现看AQS的原理及应用
动画演示AQS的核心原理