AQS源码分析——一步一步带你走进AQS的世界(一)

510 阅读8分钟

源码分析AQS

AQS结构

// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
private transient volatile Node head;

// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
private transient volatile Node tail;

// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;

// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer

AQS的等待队列示意图如下所示,AQS中的等待队列是一个双向链表,但是等待队列中不包含头节点,头节点是已经拿到了锁的节点。

等待队列中的每一个线程都被包装成了一个Node,是一个双向链表,Node的源码如下所示:

static final class Node {
    // 标识节点当前在共享模式下
    static final Node SHARED = new Node();
    // 标识节点当前在独占模式下
    static final Node EXCLUSIVE = null;

    // ======== 下面的几个int常量是给waitStatus用的 ===========
    /** waitStatus value to indicate thread has cancelled */
    // 代码此线程取消了争抢这个锁
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    // 下面会分析 等待条件队列 不是阻塞队列
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    // 同样的不分析,略过吧
    static final int PROPAGATE = -3;
    // =====================================================


    // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
    // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
    //    ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的
    volatile int waitStatus;
    // 前驱节点的引用
    volatile Node prev;
    // 后继节点的引用
    volatile Node next;
    // 这个就是线程本尊
    volatile Thread thread;
    //这个是后面的条件队列,后面会详细介绍
    Node nextWaiter;
}

看到Node的源码中有一个属性值为SIGNAL,它表示当前节点的后继节点需要被唤醒,这个需要好好的牢记并且去理解它,从这句话也可以看出来当前Node被唤醒是需要被当前Node的前继节点来唤醒的,依靠的是前面的一个好大哥。

Node的数据结构目前只需要记住有waitStatus+thread+pre+next四个属性。

ReentrantLock

介绍AQS之前,我们先来简单学习一下ReentrantLock,下面看看ReentrantLock的使用方法,它也是锁,是一个显示锁,有着比synchronized关键字更加灵活的使用方式。

public class ReentrantDemo {
    // 使用static,这样每个线程拿到的是同一把锁,当然,spring mvc中service默认就是单例,别纠结这个
    private static ReentrantLock reentrantLock = new ReentrantLock(true);

    public void createOrder() {
        // 比如我们同一时间,只允许一个线程创建订单
        reentrantLock.lock();
        // 通常,lock 之后紧跟着 try 语句
        try {
            // 这块代码同一时间只能有一个线程进来(获取到锁的线程),
            // 其他的线程在lock()方法上阻塞,等待获取到锁,再进来
            // 执行代码...
            // 执行代码...
            // 执行代码...
        } finally {
            // 释放锁
            reentrantLock.unlock();
        }
    }
}

ReentrantLock使用Sync管理锁的加锁与释放,Sync继承AbstractQueuedSynchronizer,Sync有两个实现类,分别是非公平锁和公平锁。

abstract static class Sync extends AbstractQueuedSynchronizer {
    public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
	}
}

下面分析公平锁部分,也就是FairSync。

加锁部分

static final class FairSync extends Sync {

        // 序列化使用 不分析
        private static final long serialVersionUID = -3000897897090466540L;

        // 加锁
        final void lock() {
            acquire(1);
        }

        // 下面这个方法是AQS中的acquire方法,上面的方法就是跳到下面这个方法中
        public final void acquire(int arg) {
            // 尝试获取锁 如果获取锁成功则直接返回
            // 如果获取锁失败,则调用acquireQueued(...)方法
            if (!tryAcquire(arg) &&
                // addWaiter(...)方法就是把当前线程封装成Node节点 并加入到阻塞队列尾部
                // acquireQueued(...)这个方法非常重要 真正的线程挂起 唤醒获取锁 都在这个方法中
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

        // 通过名字可以看出 是尝试获取锁
        // 尝试获取锁 获取成功的场景有两种
        // 1 没有线程在争抢锁 获取锁成功
        // 2 可重入锁的情形
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取当前锁状态
            int c = getState();
            // 如果当前锁状态为0 表示目前没有线程获取到锁
            if (c == 0) {
                // hasQueuedPredecessors()方法 查看当前节点的前面有没有节点在排队
                // 因为这是公平锁 所以需要进行一个判断
                if (!hasQueuedPredecessors() &&
                    // 此时代表没有节点在等待 此时使用CAS去尝试持有锁
                    compareAndSetState(0, acquires)) {
                    // CAS成功代表成功获取锁 将当前线程设置到锁中 表示目前是当前线程获取到锁
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 进入到这个分支 表示当前锁的持有者已经是当前线程了 也就是可重入锁的意思
            else if (current == getExclusiveOwnerThread()) {
                // 这里将锁的状态值进行改变 注意 这里是有可能加锁也有可能解锁
                int nextc = c + acquires;
                // 锁状态值 < 0 报错
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 设置锁状态值
                setState(nextc);
                return true;
            }
            // 如果上面都没有返回出去的话 表示获取不到锁 返回false
            return false;
        }

        // 上面是分析了尝试获取锁 如果尝试获取锁一旦失败 那么就进入到以下方法中
        // acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        // 所以我们首先看addWaiter方法
        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;
            if (pred != null) {
                // 注意这里 这里是直接将 当前节点的prev指向了tail节点
                // 这里没有使用CAS的方式!!! 后面会讲到这个知识点
                node.prev = pred;
                // 使用CAS的方式将tail节点替换为node
                if (compareAndSetTail(pred, node)) {
                    // 替换成功之后 将指针补全
                    pred.next = node;
                    return node;
                }
            }
            // 如果能走到这里说明两种情况
            // 1 当前阻塞队列为空 head = tail = null
            // 2 CAS操作失败 存在竞争入队
            enq(node);
            return node;
        }
        // 采用自旋的方式进行入队
        // 自旋就是 上面一次CAS失败了 那我就一直重复的去进行CAS操作 总有一次是成功的
        private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                // 上面说过 如果队列为空 也会走到这里来
                if (t == null) { // Must initialize
                    // 如果为空 则CAS操作进行初始化头节点
                    if (compareAndSetHead(new Node()))
                        // 设置成功之后 将尾节点指向头节点
                        // 注意这里没有 return 而是进入下一次循环 也就是下面的else分支
                        tail = head;
                } else {
                    // 这里就是不断重复的去进行CAS重试
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
        // 注意现在要回到acquireQueued(..., arg))这个方法
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    // 这里是获取当前Node前序节点
                    final Node p = node.predecessor();
                    // 这里进行判断 如果前序节点是头节点 说明当前节点可以去进行尝试获取锁
                    // 前面已经说过了 head 节点是已经获取了锁的节点 阻塞队列中不包含head节点
                    if (p == head && tryAcquire(arg)) {
                        // 如果获取锁成功 则把当前节点设置为头节点
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    // 进入到这个分支说明
                    // 1 当前节点的前序节点不是头节点
                    // 2 尝试获取锁失败 没有竞争过别人
                    // 到这里说明没有抢到锁 是否需要挂起当前线程
                    // 注意 这里是死循环
                    // 注意传参 一个是当前节点的前序节点 一个是当前节点
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                // 什么时候 failed 为 true 呢?
                // 当tryAcquire()方法抛出异常的时候
                if (failed)
                    cancelAcquire(node);
            }
        }

        // 注意进入到这里是没有抢到锁        
        // pred 前序节点
        // node 当前节点
        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            // 获取前序节点的状态值 注意 是获取前序节点
            // 在文章开头的时候就已经说过了 唤醒节点 是依赖前序节点的
            int ws = pred.waitStatus;
            // 如果前序节点的状态值为 -1 说明前序节点状态正常 当前线程需要被挂起 直接可以返回true
            if (ws == Node.SIGNAL)
                /*
                 * This node has already set status asking a release
                 * to signal it, so it can safely park.
                 */
                return true;
            // 如果前序节点的状态值 > 0 说明前序节点已经取消了排队
            // 前面说过 节点的唤醒需要依赖节点的前序节点
            // 就是需要找到一个好大哥 下面这部分就是帮当前节点找到一个好大哥
            if (ws > 0) {
                /*
                 * Predecessor was cancelled. Skip over predecessors and
                 * indicate retry.
                 */
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                // 进入到这个分支 说明 节点的状态值 = 0 -2 -3
                // 在前面的源码中 没有看到过有改变节点状态值的方法
                // 正常来说每个新入队的node的状态值都为0
                // 这一步就是将前序节点的状态值设置为-1
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            // 如果返回了false 会回到上一个方法 再走一遍循环
            // 再走一遍循环 会到第一个if分支返回true
            return false;
        }
        // 我们现在来分析一下这个方法 shouldParkAfterFailedAcquire()
        // 首先这个方法返回true 表示 当前线程需要挂起 则调用 parkAndCheckInterrupt()方法挂起线程
        // 如果返回false 表示当前线程不需要挂起 然后回到for循环的开始 重点来了 for循环的开始
        // for循环的开始表示再次判断 当前节点的前序节点 是否是 head节点 如果是 则尝试获取锁
        // 所以这个方法返回false的时候 就是为了避免node已经是head节点的后序节点了
        // 如果shouldParkAfterFailedAcquire()方法返回true 表示需要挂起 则使用 LockSupport的park方法简单的挂起线程
        // 被挂起之后就是等待唤醒了
    	private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            return Thread.interrupted();
        }
    }

解锁部分

加锁部分已经分析完成了,接下来就是介绍解锁部分,正常情况下,线程没有获取到锁,会一直阻塞在LockSupport.park(this);这行代码,挂起,然后等待唤醒。

// 解锁的方法如下
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// ReentrantLock中tryRelease方法
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否完全释放锁
    boolean free = false;
    // 下面就是判断是否是可重入锁的情况了 如果c==0 也就是说没有嵌套锁了 可以释放了 否则还不能释放掉
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

// 唤醒后继节点
// 从上面调用处知道,参数node是head头结点
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;
    // 如果head节点当前waitStatus<0, 将其修改为0
    if (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.
     */
    // 下面的代码就是唤醒后继节点 但是有可能后继节点取消了等待
    // 从队尾往前找 找到waitStatus<=0的所有节点中排在最前面的
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后往前找 不必担心中间有节点取消(waitStatus==1)的情况
        // 注意这里为什么不用担心 就是因为前面阻塞队列入队的操作了
        // 不理解的可以回去看看阻塞队列入队操作
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒线程
        LockSupport.unpark(s.thread);
}

线程被唤醒之后,从阻塞的那行代码开始执行,如下所示:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 刚刚线程被挂起在这里了
    return Thread.interrupted();
}
// 又回到这个方法了 acquireQueued(final Node node, int arg) 这个时候,node的前驱是head了

总结——加锁/解锁

在并发环境下,加锁和解锁需要三个部件的协调:

  1. 锁状态,也就是锁state值,这个值就是表示是否有线程持有锁的标志位。加锁就是CAS操作这个state值加1,解锁就是CAS操作这个state值减1。
  2. 线程的挂起和唤醒,AQS中使用了LockSupport的park()方法来挂起线程,unpark方法来唤醒线程。
  3. 阻塞队列,争抢锁的线程可能有很多个所以会存在阻塞队列去管理这些没有抢到锁的线程,AQS使用的是一个FIFO的队列,双向链表的形式去管理。AQS采用了CLH锁的变体来实现。