AbstractQueuedSynchronizer独占锁的获取及释放源码解析

409 阅读6分钟

介绍

AbstractQueuedSynchronizer同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这三个方法

子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。

以上概念内容参考 链接:juejin.cn/post/684490…

源码分析

同步队列

当共享资源被多个线程抢占时,未抢到锁的线程将会被阻塞,进入同步队列。AQS的同步队列是一个持有头尾指针的双向链表结构。

private transient volatile Node head;

private transient volatile Node tail;
  • head和tail记录链表头尾
static final class Node {

    static final int CANCELLED = 1; // waitStatus 状态 取消标识
    
    static final int SIGNAL = -1; // waitStatus 状态 唤醒标识
    
    static final int CONDITION  = -2; // waitStatus 状态 等待条件
    
    static final int PROPAGATE = -3; // waitStatus 状态 共享锁锁biaoshi
    
    volatile int waitStatus;

    volatile Node prev;

    volatile Node next;

    volatile Thread thread;

    Node nextWaiter;
}
  • Node类中指向前置及后置节点
  • waitStatus节点等待状态,大家可以先看一下,了解一下,后面同步状态的获取需要用到

结构如图:

2021-09-01_212849.png

获取独占锁

下面我们根据源码一起看下独占锁的获取过程

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  // 调用其子实现类进行尝试获取锁,失败进入同步队列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

进入同步队列这里有两个方法,其中addWaiter()将当前线程封装成Node节点并添加到同步队列的队尾处;

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

这一段代码就不详细解释了,比较简单,可以跟着我的流程图来理解

2021-09-01_214453.png

下面主要看下acquireQueued()方法如何实现同步队列中的节点获取锁

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)) { // 判断前驱节点是否为head,并且当前节点获取同步状态成功
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted; // 返回是否被中断的标识,这里是自旋的唯一出口
            }
            // 获取同步状态失败进入该方法
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 判断获取同步状态失败后是否需要中断的方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 获取前置节点的等待状态
    if (ws == Node.SIGNAL) // 前置节点等待状态为唤醒标识
        return true;   
    if (ws > 0) { // 前置节点等待状态为取消标识
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { // 其余(未设置-0、条件、共享状态),此处是未设置-0
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // CAS将前置节点等待状态更改为唤醒标识
    }
    return false;
}

// 线程中断
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 将当前线程中断阻塞
    return Thread.interrupted();
}

2021-09-01_221443.png

图中最左侧不应该返回false,而是interrupted的值

看到这里大概了解整个流程:

  1. 获取同步状态失败后,将当前线程封装成Node并追加到链表尾部(addWaiter方法)
  2. 针对当前节点进行自旋判断其前置节点是否为头节点,并且再次获取同步状态成功,则将当前节点设置为头结点返回interrupted的值
  3. 如果再次获取同步状态失败,则根据前置节点的等待状态来进行相应的处理,shouldParkAfterFailedAcquire
    • SIGNAL----返回true
    • CANCELLED----向前递归查找waitStatus不大于0的节点,并设置为当前节点的前驱节点,返回false
    • 其他----将前驱节点的waitStatus设置为SIGNAL(新创建节点waitStatus未指定为0),返回false
  4. 如果shouldParkAfterFailedAcquire返回true,则对当前节点进行中断处理(同步状态释放的时候会进行中断状态的唤醒),否则自旋重新进入第二步的判断

从这里也可以看出来,节点在进入同步队列后就开始自旋判断是否获取到同步状态或者将自身线程中断等待前置节点释放同步状态的时候来唤醒自身线程并再次进入自旋进行同步状态的获取。

获取同步状态的非公平性体现在哪里?

这里大家可以思考下,进入同步队列后自然是一个排队等待的状态,因为需要前置节点来唤醒后置节点,但是对于还未进入同步队列的线程不就具备同时竞争锁的权利了,所以非公平性就体现在这里,后面看公平锁的时候也能够有所发现

好,继续 之前我们说到当节点获取到同步状态后acquireQueued方法会返回是否中断的标识,再回到我们的acquire方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  // 调用其子实现类进行尝试获取锁,失败进入同步队列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

如果acquireQueued返回的中断标识为false的话,方法结束,线程获取到锁进行后续的业务操作 如果acquireQueued返回的中断标识为true的话,进行selfInterrupt()的执行,将当前线程的中断状态标识为true,但并不会中断线程,可能会在其他地方有用,做中断的处理,但是我这边目前还没发现

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

好了,至此同步状态的获取就说完了。

独占锁释放

同样根据源码看下释放的过程

public final boolean release(int arg) {
    if (tryRelease(arg)) { // 调用子实现类进行同步状态的释放
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 后置节点的处理
        return true;
    }
    return false;
}

// 后置节点的唤醒
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

2021-09-01_221650.png

独占锁的释放代码比较简单,就不详细说明了。

这篇文章也是笔者在阅读源码时根据自己的理解写的,其中如有不准确的地方,麻烦指正,感谢。