【AQS源码】深入理解AQS的工作原理

目录

一、AQS概述

二、AQS类图结构和重要属性

三、AQS源码解析之加锁流程

1)、tryAcquire()

2)、addWaiter()

3)、acquireQueued()

四、AQS源码解析之解锁流程

五、公平锁与非公平锁在AQS实现中的区别?

六、独占锁和共享锁


一、AQS概述

AQS,全称为Abstract Queued Synchronizer,译为抽象的队列同步器。它是java.util.concurrent包中,它提供了一套完整的同步编程框架。我们常用的ReentrantLock(可重入锁)、CountDownLatch(门闩)、ReadWriteLock(读写锁)等同步器都是基于AQS实现的,它们在实现锁的过程中都是依赖AQS来完成核心的加锁/解锁逻辑的,有了AQS,这些同步器就无需关心底层线程调度的细节,只需要实现他们各自的逻辑即可,相当于AQS抽象了整体的流程,然后使用模板方法设计模式,将一些需要同步器自己实现的逻辑暴露出去,强制子类自己实现。

在AQS内部,通过一个volatile的int类型的变量state来控制全局同步状态,0表示当前没有线程占有锁,可以直接尝试加锁,非0表示锁已经被占有了,需要排队。AQS结合一个先进先出(FIFO)的等待队列,实现了排队和阻塞机制。

二、AQS类图结构和重要属性

从类图可以看出,在ReentrantLock中定义了AQS的子类Sync,Sync类是继承自AQS抽象队列同步器的,可以通过Sync实现对于加锁/解锁,并且Sync在ReentrantLock中有两个实现子类:NonfairSync(非公平锁)和FairSync(公平锁)。

AQS底层维护了一个先进先出(FIFO)的双向队列,这个队列是基于链表实现的,如果线程竞争锁失败,那么就会进入到这个同步队列中进行等待。当获得锁的线程释放锁之后,会从队列中唤醒一个线程。

双向队列是基于Node节点实现的,当线程需要入队列等待锁时,会将每个过来获取锁的线程、节点状态等信息封装成一个Node对象,进行入队操作。

Node节点结构如下:

// 将正在等待获取锁的线程封装为链表的一个节点
static final class Node {
    // 共享模式
    static final Node SHARED = new Node();
    
    // 独占模式
    static final Node EXCLUSIVE = null;

    /**
     * 当前节点的等待状态
     */
    // 线程取消:1
    static final int CANCELLED =  1;
    
    // 线程等待唤醒:-1. 当前节点在释放或取消锁时必须唤醒其后继节点
    static final int SIGNAL    = -1;
    
    // 线程等待:-2
    static final int CONDITION = -2;
    
    // 传播:-3。用于共享锁,表示共享式同步状态的传递
    static final int PROPAGATE = -3;

    // 节点等待状态
    volatile int waitStatus;

    // 前驱节点
    volatile Node prev;

    // 后继节点
    volatile Node next;

    // 节点入队的线程,node节点中存放的都是一个个Thread
    volatile Thread thread;

    // 队列中下一个等待者
    Node nextWaiter;

    // 队列中的下一个对象是否为共享式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 获取当前节点的前驱节点,为空则抛出空指针异常
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

Node节点的状态(waitStatus)有如下四种:

  • CANCELLED = 1:表示当前节点从同步队列中取消,即当前线程被取消;
  • SIGNAL = -1:表示后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行;
  • CONDITION = -2:表示当前节点在等待condition,也就是在condition queue中,只有使用到condition时才有;
  • PROPAGATE = -3:表示下一次共享式同步状态获取将会无条件传播下去;

AQS中几个重要的属性: 

// 等待队列的头节点(队头)
private transient volatile Node head;

// 等待队列的尾节点(队尾)
private transient volatile Node tail;

// AQS的同步状态,使用volatile修饰保证其可见性
private volatile int state;

// 直接操作内存的unsafe工具包,主要是用来执行CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();

// 各个变量的内存地址偏移量
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

接下来我们基于ReentrantLock来分析AQS源码,了解其获取锁、释放锁流程。

三、AQS源码解析之加锁流程

首先编写一个ReentrantLock的简单示例:这里涉及到三个线程抢占锁。

public class AQSDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁...");
            try {
                // 模拟耗费一段时间
                TimeUnit.SECONDS.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            lock.unlock();
        }, "线程A").start();

        new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁...");
            lock.unlock();
        }, "线程B").start();

        new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁...");
            lock.unlock();
        }, "线程C").start();
    }
}

前面介绍到,在ReentrantLock类中,定义了一个Sync类,而Sync又继承自AQS抽象队列同步器,如下图:

先看下ReentrantLock类的构造方法:

public ReentrantLock() {
    // 默认创建的是非公平锁
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    // 传true的话,创建公平锁; 传false的话,创建非公平锁
    sync = fair ? new FairSync() : new NonfairSync();
}

sync成员ReentrantLock提供了两种模式获取锁:

  • 非公平锁:默认创建的就是非公平锁。简单理解,非公平锁,就是真正获取到锁的顺序,并不一定是申请锁时候的顺序,有可能,最后一个线程过来申请锁,却是最先成功获取锁的线程。
  • 公平锁:公平锁则是严格遵循先来后到,先来申请锁资源的线程,肯定也是先获取到锁的。

这里主要分析非公平锁的独占方式获取锁/释放锁整体流程。

非公平锁的独占加锁入口: 

public void lock() {
    sync.lock();
}

如上图,Sync的lock()方法是一个抽象方法,由子类实现,这里是非公平锁,所以我们查看NonfairSync#lock方法。 

final void lock() {
    // 非公平锁,比较暴力,一上来就先执行一次CAS抢占锁,如果CAS失败,则执行acquire(1)标准获取锁流程
    if (compareAndSetState(0, 1))
        // 设置当前独占线程,就是当前线程(exclusiveOwnerThread)
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 调用AQS的模板方法acquire(int arg)加锁
        acquire(1);
}

基于案例代码,线程A率先执行lock.lock()来申请获取锁,所以它比较暴力,上来直接CAS修改state,因为此时没有其它线程持有锁,所以全局的同步状态state为0,CAS操作能执行成功,然后线程A就调用setExclusiveOwnerThread()方法将它作为当前独占这把锁的线程。

此时AQS的状态图如下:

我们看到,非公平锁有点不讲武德,一上来,就先直接一下CAS想把全局同步状态state改成1,这也是与公平锁的区别之一,我们对比一下公平锁的lock()方法。

显然,公平锁就比较优雅。

前面介绍到锁已经被线程A获取了,那么此时轮到线程B调用sync.lock()方法:

下面我们看一下acquire()方法:

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

获得独占锁就是从acquire()方法开始的。这个方法中分别调用了三个方法:tryAcquire()、addWaiter()、acquireQueued()。

这个方法的主要逻辑是:

  • 通过tryAcquire()尝试获取独占锁,如果成功返回true,失败返回false;
  • 如果tryAcquire()失败,则会通过addWaiter()将当前线程封装成Node添加到AQS队列尾部;
  • 执行acquireQueued(),将Node作为参数,通过自旋去尝试获取锁,将线程抢占不到资源后挂起;

1)、tryAcquire()

tryAcquire()是AQS中的一个模板方法,强制子类去实现,否则抛出UnsupportedOperationException异常

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

这里是非公平锁,所以我们查看NonfairSync#tryAcquire中的实现:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

// 执行非公平的尝试加锁
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取当前AQS的state同步状态的值
    int c = getState();
    // 如果为0, 会尝试使用CAS获取锁. 因为有可能锁刚好被别的线程释放了,所以这里尝试加锁
    // 需要注意的是,非公平锁在这里不会执行hasQueuedPredecessors()判断是否有任何线程等待获取的时间比当前线程长,直接尝试加锁。
    if (c == 0) {
        // CAS更新state
        if (compareAndSetState(0, acquires)) {
            // 如果CAS成功拿到锁,则将当前线程设置为持有这把锁的线程,并返回
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果state不为0,说明有线程正在持有锁。这里其实是判断锁的重入。判断当前持有锁的线程是不是自己, 如果是自己的话,则直接更新state的值即可。
    else if (current == getExclusiveOwnerThread()) {
        // 累加state,可以理解为: 锁的重入次数加上acquires次
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 其它情况返回false
    return false;
}

 对照如上代码,线程B执行nonfairTryAcquire():

  • a、先获取到当前线程:线程B
  • b、获取到当前同步状态state = 1
  • c、判断state不为0,不会执行cas操作
  • d、判断当前持有锁的线程是不是当前线程,很显然,当前线程是线程B,当前持有锁是线程A;
  • e、于是线程B执行nonfairTryAcquire()返回false,表示线程B获取锁失败;

线程B执行tryAcquire()返回false:

那么取反之后就是true,那么线程B还需执行下一步:addWaiter(Node.EXCLUSIVE)以独占方式加入等待队列。

2)、addWaiter()

addWaiter()整体流程为:将当前线程封装成一个Node对象,然后判断等待队列的尾节点是否为空,不为空的话,则将新创建的Node执行入队操作,修改链表指向;如果尾节点为空,则调用enq()执行入队,enq()内部是一个自旋操作,知道入队成功。 

// 将当前线程根据给定模式(独占/共享)封装成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;
    // 如果等待队列的尾节点不为空
    if (pred != null) {
        // 修改队列指针: 当前Node节点的前驱节点指向等待队列的尾节点
        node.prev = pred;
        // CAS设置当前Node节点为新的等待队列的尾节点
        if (compareAndSetTail(pred, node)) {
            // 修改队列指针: 原等待队列的尾节点的后继节点指向当前Node节点
            pred.next = node;
            return node;
        }
    }
    // 如果等待队列的尾节点为空, enq()执行入队操作, enq()采用自旋的方式,直到入队成功 (本方法内部采用自旋)
    enq(node);
    // 返回封装的Node节点
    return node;
}

我们还是对照案例,这里线程B执行addWaiter(),进来判断tail尾节点是否为空,因为当前AQS的状态图如下:

此时尾节点tail为空,所以会执行enq()方法: 

// 将Node节点插入到等待队列中,这里还涉及到第一次获取锁的时候,等待队列需要初始化
private Node enq(final Node node) {
    // 自旋
    for (;;) {
        // 获取等待队列的尾节点
        Node t = tail;
        // 当第一个线程来获取锁的时候,此时等待队列是空的,即等待队列的尾节点为null,此时需要执行队列的初始化
        if (t == null) { // Must initialize
            // 直接创建了一个空的Node, 作为虚拟节点存在, 采用CAS将这个Node设置为等待队列的头节点
            if (compareAndSetHead(new Node()))
                // 等待队列的尾节点也指向这个空的虚拟节点.
                // 即执行等待队列初始化的时候,首节点、尾节点都是指向新创建的这个虚拟Node节点
                tail = head;
        } else {
            // 修改队列指针指向:当前Node节点的前驱节点 指向 等待队列的尾节点
            node.prev = t;
            // CAS设置尾部节点为当前Node节点,即当前Node节点成为等待队列新的尾节点
            if (compareAndSetTail(t, node)) {
                // 修改队列指针指向: 等待队列旧的尾节点的后继节点 指向 当前Node(等待队列新的尾节点)
                t.next = node;
                // 返回等待队列旧的尾节点
                return t;
            }
        }
    }
}

线程B进来,获取等待队列的尾节点tail,tail == null返回为true,说明此时AQS的队列还未初始化,我们需要先初始化。

这里直接通过new Node()创建了一个空的Node对象,此节点相当于一个虚拟节点,或者说是哨兵节点,然后设置为队列的头节点,并把它指向尾节点。此时AQS的状态图如下:

此时,队列已经完成了初始化,因为enq()里面是一个for (;;)自旋,所以线程B第二次执行的时候,这一次尾节点tail已经不为空了,所以会走下面的逻辑:

将线程B对应Node节点加入到等待队列中,并修改前驱、后继节点的指向。此时AQS的状态图如下:

此时线程B的addWaiter()方法已经执行完成,接下来将会执行acquireQueued()方法。

3)、acquireQueued()

acquireQueued()的主要作用是将抢占不到资源后的线程直接park挂起。

// 将线程抢占不到资源后挂起
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 这里又是一个自旋
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 判断前驱节点是不是就是head头节点

            // 如果前驱节点就是head头节点的话,则再次调用tryAcquire尝试获取锁
            // 需要注意的是,这块代码包含在for自旋中,也就是说,如果某个线程之前被park()之后,当它被unpark()唤醒之后,还会继续执行下面的代码:判断前驱节点是头节点,并且再次尝试获取锁
            if (p == head && tryAcquire(arg)) {
                // 获取锁成功的话,则设置当前Node节点为新的等待队列的头节点, 并将当前Node节点的线程、前驱节点都置空
                setHead(node);
                // 前驱节点的后继节点也置空
                p.next = null; // help GC  帮助GC回收,实际上就是将之前的虚拟头节点进行出队,原先虚拟头节点的后继节点成为新的虚拟头节点
                failed = false;
                return interrupted;
            }
            // 检查并更新未能获取的节点的状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果标志位failed还是true的话,则取消尝试获取锁
        if (failed)
            cancelAcquire(node);
    }
}

可以看到,acquireQueued()里边又是一个for(;;)自旋,还是对照案例说明一下。

线程B进来,此时线程B的前驱节点就是head头节点,但是因为线程A还未释放锁,所以tryAcquire()尝试获取锁肯定也是失败的,返回false。

根据上面代码会进入shouldParkAfterFailedAcquire()方法: 

// 检查并更新未能获取的节点的状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前驱节点的等待状态
    int ws = pred.waitStatus;
    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);
        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;
}

此时线程B的前驱节点的等待状态waitStatus=0,执行流程如下图:

这里将会调用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)将线程B的前驱节点,也就是目前的head节点的waitStatus修改为-1,然后shouldParkAfterFailedAcquire(p, node)方法返回false,不会执行if里面的逻辑。

此时AQS的状态图如下:

因为是一个自旋,所以线程B第二次执行同样的逻辑:

线程B再次执行shouldParkAfterFailedAcquire()方法:

所以shouldParkAfterFailedAcquire(p, node)第二次执行是返回true,需要执行parkAndCheckInterrupt()方法:

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

可以看到,this就是线程B,也就是说,线程B调用了LockSupport.park()在这里阻塞等待获取锁了。

跟线程B获取锁流程类似,因为线程A执行比较耗时,还未释放锁,此时线程C也来获取锁,tryAcquire(arg)肯定返回false,同样会执行线程C的addWaiter()入队:

此时AQS的状态图如下:

同样的,线程C在执行acquireQueued()里边的shouldParkAfterFailedAcquire(p, node)时,会将前驱节点,也就是线程B的Node的等待状态waitStatus修改为-1。此时AQS的状态图如下:

接着,线程C也执行park方法阻塞等待获取锁:

至此,线程B、线程C都在acquireQueued()方法中阻塞着,等待其他线程unpark之后再次去尝试获取锁,如下图:

以上就是AQS中独占锁加锁、入队的详细过程。

四、AQS源码解析之解锁流程

AQS的解锁入口:

public void unlock() {
    sync.release(1);
}

同样调用的是sync的release()方法,由于sync类继承自AQS,并且没有重写release()方法,所以这里实际上调用的是AQS的release()方法:

public final boolean release(int arg) {
    // tryRelease(): 尝试释放锁。返回true,表示当前线程释放锁成功
    if (tryRelease(arg)) {
        // 获取等待队列的头节点
        Node h = head;
        // 等待队列的头节点不为空,并且节点等待状态不等于0
        if (h != null && h.waitStatus != 0)
            // 唤醒等待队列的头节点的后继节点(如果存在的话)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先会调用tryRelease()方法尝试释放锁,跟tryAcquire()一样,tryRelease()也是一个模板,AQS强制其子类去实现:

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

我们直接看ReentrantLock.Sync#tryRelease方法:

protected final boolean tryRelease(int releases) {
    // 计算AQS当前状态state的值减去releases后的值
    int c = getState() - releases;
    // 如果释放锁的线程不是当前正在持有锁的线程,将会抛出IllegalMonitorStateException异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 释放锁是否成功标识
    boolean free = false;
    // 如果state已经减到为0,说明当前线程已经完成退出锁
    if (c == 0) {
        // 修改标识位为true
        free = true;
        // 因为当前持有锁的线程已经释放锁,所以需将exclusiveOwnerThread置空,表示这把锁目前没有线程正在持有
        setExclusiveOwnerThread(null);
    }
    // 重新设置state的值
    setState(c);
    return free;
}

 还是对照前面的案例分析,假设线程A终于要释放锁了,进入tryRelease(1)方法:

  • 1、获取到当前AQS同步状态state,值为1,减去此次释放的1,那么得出c=0;
  • 2、判断释放锁的线程是不是当前正在持有锁的线程,正常情况下都是,如果不是,将会抛出IllegalMonitorStateException异常;
  • 3、因为c==0,调用setExclusiveOwnerThread()将当前持有锁的线程置空;
  • 4、重新设置AQS同步状态state的值为0,tryRelease(1)方法返回true;

先来看下当前AQS的状态图:

进入if判断,首先获取到队列头节点,此时队列头节点肯定不为空,并且节点等待状态waitStatus=-1,不等于0,那么就会执行unparkSuccessor(h)方法,唤醒等待队列中头节点后面一个节点:

// 唤醒node节点的后继节点(如果存在)
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.
     */
    // 获取node节点的等待状态
    int ws = node.waitStatus;
    // 节点等待状态如果小于0的话,则使用CAS将其waitStatus修改为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.
     */
    // 获取到node节点的后继节点
    Node s = node.next;
    // 后继节点为空 或者 后继节点的等待状态大于0(取消获取锁)
    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)
        // 调用unpark()唤醒节点对应的线程
        LockSupport.unpark(s.thread);
}

执行完前面的unpark后,此时AQS的状态图如下:

由于线程B已经被unpark,还记得之前我们说过的线程B阻塞的地方么,那就是acquireQueued()方法:

线程B执行tryAcquire(),由于当前AQS的同步状态state的值为0,cas操作能成功,所以线程B抢占得到锁:

此时AQS的状态图如下:

以上就是AQS释放锁的整体流程。 

五、公平锁与非公平锁在AQS实现中的区别?

  • 非公平锁:一上来就先执行一次CAS抢锁,如果抢占不成功,执行正常的获取锁流程。并且在tryAcquire()方法中,非公平锁在这里不会执行hasQueuedPredecessors()判断是否有任何线程等待获取的时间比当前线程长,直接尝试加锁。
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
  • 公平锁:执行正常获取锁流程。在tryAcquire()方法中,公平锁会执行hasQueuedPredecessors()判断是否有任何线程等待获取的时间比当前线程长,严格遵循先来后到。
final void lock() {
    acquire(1);
}

六、独占锁和共享锁

  • 独占锁:指该锁只能同时被一个线程持有;
  • 共享锁:指该锁可以被多个线程同时持有;

举个生活中的例子,比如我们使用打车软件打车,独占锁就好比我们打快车或者专车,一辆车只能让一个客户打到,不能两个客户同时打到一辆车;共享锁就好比打拼车,可以有多个客户一起打到同一辆车。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值