AQS 之 互斥锁 源码剖析

AQS 之 互斥锁 源码剖析

AQS 是 AbstractQueuedSynchronizer 类的简称,AQS 是一个用来构建锁和同步器的基础框架,想要了解 Java 的锁实现及其底层原理就必须先了解 AQS 完成了什么,提供了哪些功能。有了AQS的基础支撑我们后面再去学 Java 锁(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)相关类的源码时就会觉得很轻松。那么下面我们就开始进入 AQS 的源码之旅。

首先我们先来看看 AQS 的核心成员信息:

// 用于表示队列当前的头尾节点
private transient volatile Node head;
private transient volatile Node tail;
// 核心变量,如何获取、释放锁操作均是通过对该变量的原子性操作来实现的,后面看具体实现类就知道了
private volatile int state;
// Node 包装类,线程信息,节点状态信息,用于链表实现
static final class Node {
        // 用于标识 Node 节点属于共享模式
        static final Node SHARED = new Node();
        // 用于标识 Node 节点属于独占模式
        static final Node EXCLUSIVE = null;
        // 节点处于取消状态
        static final int CANCELLED =  1;
        // 当前节点处于该状态时表明其后继结点需要被唤醒,标识当前节点需要唤醒后继结点
        static final int SIGNAL    = -1;
        // 标识当前节点阻塞等待在条件队列上
        static final int CONDITION = -2;
        // 共享锁模式下,共享锁唤醒后继结点时通过该值标识后继结点是否需要继续被唤醒,唤醒传播行为
        static final int PROPAGATE = -3;
		// 取值为上述 4 种
        volatile int waitStatus;
		// 双向链表的标准实现方式,用于指向当前节点的前后节点
        volatile Node prev;
        volatile Node next;
    	// 节点所绑定的线程信息
        volatile Thread thread;
    	// 用于 condition 条件队列的实现,单链表实现
        Node nextWaiter;
    	// 获取当前节点的前一个节点信息
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
    }

有了上述的信息,下面我们来看看 AQS 是如何使用这些信息实现同步机制的。

获取锁原理

何为互斥?互斥就是指有一段代码块同一时刻只有一个线程可以访问该代码片段,其他线程只有等待该线程执行完才能访问该代码片段。AQS 中是如何实现该特性的呢?通过上述的信息我们发现有个 state 变量,AQS 就是通过操作该变量来实现同一时刻保证只有一个线程进入互斥区执行代码。多线程同时操作 state 变量,又如何保证其原子性的呢?答案是 CAS (compare and swap)操作。谁 CAS 成功,谁就获得执行权,CAS 失败的线程又该如何处理呢?放入一个队列,当上一个线程释放了 state 唤醒等待在队列中的线程。了解的这些,下面我们来看具体源码实现。

acquire方法原理

tryAcquire方法是 AQS 提供的钩子函数,有具体子类实现如何获取锁逻辑,子类返回结果告诉父类是否获取锁成功。如果获取失败调用acquireQueued方法,线程加入队列并LockSupport.park将其阻塞。

public final void acquire(int arg) {
    if (!tryAcquire(arg) && // tryAcquire 由子类实现
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // addWaiter 方法降线程添加到等待队列,acquireQueued方法中执行线程阻塞操作
        selfInterrupt(); // 如果线程发生中断并且中断标志位被清除了,重新设置中断标志位
}
addWaiter方法原理

1、创建 Node 节点,包装线程信息和当前节点模式信息(EXCLUSIVE模式,互斥模式)

2、如果队列已经初始化,尝试直接将 Node 节点加入队列,大家记住一点,多线程场景下此步添加并不一定能成功,添加失败则进入 enq 方法,for(;;)加 CAS 操作保证节点一定插入成功。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); // 创建 Node 信息
    Node pred = tail; // tail 不为空标识队列已经初始化,尝试插入队列即可
    // 次 if 分支属于条件前置优化
    if (pred != null) {
        node.prev = pred;// 此步优先将 Node 的 prev 引用指向原队列的尾结点,为何先关联 prev 在后续源码中你会看到,此处不做过多解释。CAS 更新 tail 指向最新的 Node 节点,CAS 成功,将 原队列的尾结点也即 pred 的 next 引用指向当前 Node 节点。
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 上述步骤执行失败,进入 enq 方法
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {// 死循环操作
        Node t = tail; // 保存当前尾结点在在栈上,保证后续操作的一致性
        if (t == null) { // 队列未初始化,进行初始化
            if (compareAndSetHead(new Node()))// head 节点指向一个伪节点(不存在数据的节点)
                tail = head;
        } else {
            // 下面的代码逻辑同 addWaiter方法中 if 条件分支,
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
acquireQueued方法原理

进入此方法说明线程已经被添加到等待队列中了,接下来就该让线程进入OS阻塞了,但是 Java 并没有直接调用线程的阻塞方法让其进入OS 阻塞等待,而是做了一定的优化处理,在多线程并发处理的情况下,如果你前面的线程已经执行完并且释放了锁呢?你是否可以尝试着再去获取一下锁呢?万一你抢到了锁,是不是意味着你就不用进入 OS 阻塞等待了,进入 OS 睡眠等待,及唤醒然后从新等待 CPU 调度执行是很耗时的操作。所以 Java 层面会尽可能的减少与 OS 底层的交互以提升性能。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // 获取当前节点的前一个节点,通过 prev 引用获取
            // 如果 p 节点是头结点,调用 tryAcquire 再一次尝试获取锁,因为 p 是头结点,随时都有可能释放锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);// 获取锁成功,将自己设置为头结点,将 head 指向自己
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && // 更新当前节点的 prev 节点状态
                parkAndCheckInterrupt()) // 进入 OS 阻塞
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);// 取消流程后续讲解
    }
}
// 头结点是持有锁的节点,正在执行任务,所以 Node 并不需要保存线程信息,将其置为 null
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

// 为获取到锁,更新 pred 节点状态,帮助将已取消节点从队列中移除。(pred = node.prev)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 取到 pred 节点状态
    if (ws == Node.SIGNAL) // 状态已变更,直接返回
        return true;
    if (ws > 0) {// 如果 ws > 0 标识 pred 节点已经被取消了,那么 node 线程将负责帮忙将以取消的节点从队列中移除,将node节点挂在已取消的节点后面没有任何意思。
        do {
            node.prev = pred = pred.prev;// 标准的链表操作,
        } while (pred.waitStatus > 0);// 向前遍历查找,知道找到一个未被取消的节点
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// CAS 将 pred 的状态更新为 SIGNAL,表示 pred 需要唤醒后继结点(node 节点)
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 进入 OS 睡眠阻塞
    return Thread.interrupted();// 线程唤醒后检测线程是否发生中断并清空中断标志位
}

总结:通过上述源码我们发现 AQS 只是提供了统一共用的模板算法(模板方法设计模式),线程获取锁失败的阻塞操作是通用的,将其抽取出来放入统一模板算法当中,中间提供钩子方法(tryAcquire)由具体子类实现,不同的锁实现方式不同,它们如何操作 state 变量父类无从知晓。子类通过实现 tryAcquire 方法完成各自的获取锁逻辑。接下来要介绍的释放锁源码实现原理也是如此。

释放锁原理

release方法原理

tryRelease 钩子方法,子类实现。所释放成功返回 true,然后唤醒后续等待节点

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;// 获取当前头结点 head 
        if (h != null && h.waitStatus != 0) // 状态检测
            unparkSuccessor(h); // 实际唤醒操作
        return true;
    }
    return false;
}
unparkSuccessor方法原理

主要目的是唤醒后继结点,并将当前节点状态更新为 0,代表自己以唤醒后继结点,如果 next 节点已取消需要向前遍历找到一个未取消的节点进行唤醒。

private void unparkSuccessor(Node node) { // node 当前头结点
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0); // 将其更新为 0 
    Node s = node.next; // 获取到其 next 所指向的节点
    if (s == null || s.waitStatus > 0) {// s 为空 获已被取消
        s = null;
        // 从队列尾部向前遍历找到最早加入队列且未被取消的节点,然后将其唤醒;此步的遍历操作也就解释了为何在加入队列时是先关联其 prev 引用的原因,读者只要在脑海里有着这是在多线程并发环境下执行的,CPU如何调度你的线程执行你是无法控制的。如果在 B 刚把 prev 关联指向了前一个节点 A,此时时间片用完,CPU 重新调度执行另一个线程 A 任务,而 A 正是当前队列的头结点呢,A 执行完释放锁执行唤醒操作,此时发现自身的 next 指向 null(因为 B 还没来得及操作 tail 节点,将其与 A 的next 进行关联),但是这并不代表队列中没有任务了。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

取消锁原理

cancelAcquire方法原理

1、节点取消后无需持有线程信息,将其置空并将状态设置为 CANCELLED

2、如果 prev 节点已取消,当前线程将负责帮忙进行清理,已取消线程挂在队列中已无用

3、 如果 node 是尾结点,更新tail,将自己从尾部剔除

4、不是头结点,更新 pred 节点状态为 SIGNAL (状态不是 SIGNAL 时更新),如果 node 的 next 引用所指向的节点未被取消,那么更新pred 的 next 引用指向 node 的 next 所指向的节点,否则什么也不做,这些已取消的节点将由其他线程负责处理

private void cancelAcquire(Node node) {
    node.thread = null;

    Node pred = node.prev;
    // 将前面已取消节点从队列移除,标准链表操作
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
	// 保存 pred 的 next 节点,如果 node.prev.waitStatus < 0,那么 predNext == node;否则 predNext 的值如下图所示
    Node predNext = pred.next; 

    node.waitStatus = Node.CANCELLED;
	// 如果当前节点是尾结点,将tail更新为 pred 节点,并将 pred 与 node 之间的 next 链断开,也即置空 pred 的 next 引用
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        if (pred != head && // 不是头结点
            ((ws = pred.waitStatus) == Node.SIGNAL || // 状态已更新
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && // 更新状态为 SIGNAL
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0) 
                // 更新 pred 的 next 引用,指向 node 的 next 所指向的节点,将自己从队列移除
                compareAndSetNext(pred, predNext, next);
        } else {
            // 如果 pred 是头结点执行唤醒操作
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

AQS 之 互斥锁 源码剖析_第1张图片

你可能感兴趣的:(【JAVA】JUC,之,AQS,与,锁实现篇,java,spring,boot,程序人生,学习方法)