Java多线程——JUC之AQS(AbstractQueuedSynchronizer)分析,ReentrantLock的实现原理解析

概述

AQS是用来构建锁或者其他同步器组件的重要基础框架,是JUC体系的基石。通过内置的FIFO(先进先出队列,CLH队列,是一个单向链表,AQS的队列是CLH的一个变体,虚拟的双向FIFO)队列来完成资源获取线程的排队工作,并通过一个int类型的变量表示持有锁的状态。ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等底层都是存在一个内部类继承了AbstractQueuedSynchronizer,为这些具体的并发工具提供了一种排队等候机制

如果共享资源被占用,就需要一定的阻塞、等待、唤醒机制来保证锁的合理分配,这个机制就是使用CLH队列的变体实现的,将暂时获取不到锁的线程加入队列中等候,这就是AQS的抽象表现之一。将获取不到锁的线程封装成队列中的节点(node),通过CAS、自旋、LockSupport.park()等方式维护state变量的状态,使并发达到同步的控制效果

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配, 通过CAS完成对state值的修改。

state & Node

CLH变种的双向队列

队列Javadoc

The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue. 
CLH locks are normally used for spinlocks.  
We instead use them for blocking synchronizers, but use information about a thread in the predecessor of its node.
A "status" field in each node keeps track of whether a thread should block.
A node is signalled when its predecessor releases.
Each node of the queue otherwise serves as a specific-notification-style monitor holding a single waiting thread.
The status field does NOT control whether threads are granted locks etc though.
A thread may try to acquire if it is first in the queue.
But being first does not guarantee success;it only gives the right to contend.  So the currently released contender thread may need to rewait.

机翻(有道词典):
等待队列是“CLH”(Craig、Landin和Hagersten)锁队列的变体。
CLH锁通常用于自旋锁。
相反,我们将它们用于阻塞同步器,但使用关于其节点的前身中的线程的信息。
每个节点中的“status”字段用于跟踪线程是否应该阻塞。
当它的前身释放时,一个节点被通知。
否则,队列中的每个节点都充当一个特定通知样式的监视器,该监视器持有单个等待线程。
status字段不控制线程是否被授予锁等等。
如果线程是队列中的第一个,那么它可能会尝试获取。
但是第一并不能保证成功,它只是给了你竞争的权利。因此,当前发布的竞争者线程可能需要重新等待。

整个AQS结构图

waitStatus

  • 初始化:0
  • CANCELLED:1,表示线程获取锁的请求已经取消了
  • CONDITION:-2,表示节点在等待队列中,节点线程等待被唤醒
  • PROPAGETE:-3,当前线程处于SHARED情况下,该字段才会使用
  • SIGNAL:-1,表示当前节点的线程已经准备好了,就差资源的释放了

原理图

通过ReentrantLock解读AQS源码

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成了线程访问的控制。ReentrantLock的部分源码如下:

/** Synchronizer providing all implementation mechanics */
private final Sync sync;

/**
 * Base of synchronization control for this lock. Subclassed
 * into fair and nonfair versions below. Uses AQS state to
 * represent the number of holds on the lock.
 */
abstract static class Sync extends AbstractQueuedSynchronizer {......}

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {......}

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {......}

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

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

ReentrantLock公平锁

ReentrantLock非公平锁

非公平锁在每个线程尝试lock资源的时候,首先回去CAS修改state的状态,这就是ReentrantLock非公平锁实现中,"非公平"的体现

可以看到两者的却别仅在于公平锁中多了一个hasQueuedPredecessors的判断,用于判断当前是否存在有效等待队列。

从非公平锁入手解析AQS

ReentrantLock加锁分为三个阶段:

  1. 尝试加锁
  2. 加锁失败,线程进入队列
  3. 线程入队之后进入阻塞状态

尝试枷锁

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

compareAndSetState方法使用CAS的方式去更新state的值。实质上就是尝试加锁加锁的时候当前线程期望state=0(没有被人占用),如果state=0,那么就将其改变为1。如果CAS操作成功,就代表了当前资源已经被占用,其他线程无法进行加锁,但是此时,加锁操作并未完成。只有当setExclusiveOwnerThread方法执行完毕之后,当前线程才真正的加锁成功

重点来了,此时的state=1,已经被某个线程占用。在此之后的其他线程再次尝试加锁的时候将会进入acquire方法。

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

tryAcquire(arg)方法,该方法的代码可能会让第一次看到的人产生疑惑:

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

tryAcquire直接抛出了异常,这是什么情况?这是一种模板方法设计模式,逼着子类去重写该方法。让子类去做实际上的逻辑实现。因为当前ReentrantLock初始化的是非公平锁NonfairSync,该子类的方法如下:

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

其又调用了父类的Sync的nonfairTryAcquire方法:

final boolean nonfairTryAcquire(int acquires) {
    //当表的就是“其他线程”正在尝试加锁的线程
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

此时由于现在已经有线程占用了资源,c = 1,所以将会进行当前线程与目前占用资源的线程对比,看看是不是同一个线程,此时比较失败,该方法将会返回false。所以回到acquire方法,!tryAcquire(arg) = true

那么在nonfairTryAcquire方法中c == 0的if判断是干什么呢

其实存在一种情况,当某个线程尝试compareAndSetState加锁失败后,进入了nonfairTryAcquire方法,此时之前占用的线程正好释放了资源,state == 0,那么当前线程运气很好,正好可以再次尝试CAS加锁

那么如果当前线程 == 当前占用资源的线程做了什么事情呢?

还是存在一种特殊情况,就是占用资源的线程再次进行了lock操作,尝试再一次的加锁,所以当前nextc = c + 1,之后setState(nextc)设置新的state值,这就是ReentrantLock可重入的锁的实现

继续刚才非资源占用线程的分析,将会进入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) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

首先该方法二话不说,就将当前没有抢占用锁的线程封装为Node节点。

pred = tail来获取当前链表的尾节点。如果当前没有等待的线程,那么此时pred == tail == null,此时线程将会进入enq(node)方法:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

没明显enq方法是一个自旋操作,此时判断链表的尾指针tail是否为空,也就是判断此时链表有没有被初始化过。

  • 如果链表没有被初始化过,此时创建了一个空节点通过CAS操作设置为head节点。被称为是傀儡节点或者是哨兵节点,起到占位的作用,与整个逻辑体系关系不大。
    • 该head节点thread = null,waiteStatus = 0(int类型默认值)
    • 之后又将tail节点指向head节点,也就是刚刚创建的空节点,此时head和tail都指向了该空节点,形成了一个首尾相接的链表
  • 当第一次循环(第一次循环为了初始化链表)结束后方法并没有结束,进入下一次循环。此时将会进入else代码块,开始将当前线程节点进行入队操作
    • 将当前线程封装的节点的前指针prev指向链表的尾指针。
    • 将链表的尾指针tail通过CAS操作指向当前线程封装的node节点。
    • 再将tail的next指向当前线程封装的node,此时当前线程入队成功。

此时链表中存在一个傀儡节点和一个等待线程节点,返回addWaiter方法,当下一个线程尝试加锁失败后,此时的pred = tail不为空,进入if判断:将当前线程的prev指向链表中的tail,通过CAS操作再将自己设置为tail尾节点,设置成功后将上一个节点的next指向当前线程节点,此时就完成了入队操作,并不用再进入enq方法

此后的入队操作将会直接进入else代码块,形成的链表如原理图所示。

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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这个filed是什么意思,从finally中可以看出,如果filed = true会取消该节点排队,所以这是一决定该节点是否继续等待的标识。

进入自旋代码块,第一行node.predecessor()很简单,返回该节点的前一个节点。

final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

如果当前节点的上一个节点是哨兵节点(也就是说当前节点是链表中第一个线程节点),那么就再次调用tryAcquire方法,看看能不能获取到锁资源:

  • 如果此时锁资源没有释放,也就是说tryAcquire返回false。进入(shouldParkAfterFailedAcquire&&parkAndCheckInterrupt)判断。如果此时所资源释放了,那么也就是说tryAcquire成功,返回true,执行出队操作将当前节点移出链表。

此时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.
             */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

由于perd是哨兵节点的关系,此时ws = pred.waitStatus = 0。此时进入else代码块,通过CAS操作将哨兵节点的waitStatus设置为SIGNAL(也就是-1)。返回false,代表这个节点可能没必要继续park了。

进入下一次循环,再次执行tryAcquire尝试获取锁资源。如果还是获取不到再次进入(shouldParkAfterFailedAcquire&&parkAndCheckInterrupt)判断,此时ws = -1,也就是SIGNAL,直接返回true,开始执行parkAndCheckInterrupt内代码:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

当前节点通过LockSupport挂起,也就是说链表的第二个节点,即第一个线程节点被阻塞,等待着被唤醒。这一步的意义很重要!!!之后的节点也会执行到这一步,被挂起,可以理解为节点在这一步彻彻底底的“入队”了,此时程序不会往下执行了

ReentrantLock.unlock()

所有队列中的节点都被挂起,等待着锁资源的释放。

ReentrantLock.unlock()

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

AQS

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

//又是一个模板方法模式,需要子类进行重写
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

ReentrantLock.release()

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

每次unlock都会让state - 1,由于ReentrantLock是可重入锁,之前有提到每一次lock都会+1。当state减到0的时候,free将会 = true返回,将当前锁资源占用的线程置空

此时回到release方法,进入if判断内,头节点不为null,哨兵节点在shouldParkAfterFailedAcquire方法执行的时候就变为SIGNAL即-1,整体判断(h != null && h.waitStatus != 0) == true,进入判断内执行unparkSuccessor(哨兵节点):

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

此时哨兵节点的state == -1,即ws == -1,所以就unparkSuccessor再通过CAS操作把哨兵节点的state设置为0

s为哨兵节点的下一个节点,即第一个线程节点。s肯定不等于null,s的state也不大于0。直接执行LockSupport.unpark(s.thread)。【也就是唤醒了之前在parkAndCheckInterrupt方法中阻塞的线程

回到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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

所以parkAndCheckInterrupt方法继续执行,线程没有被中断过,返回false,判断不成立,进入下一次自旋,p仍然是哨兵节点,也就是head指向的节点,再次执行tryAcquire方法试图加锁,此时执行tryAcquire就和之前不太一样了:

final boolean nonfairTryAcquire(int acquires) {
    //当表的就是“其他线程”正在尝试加锁的线程
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

由于上一个线程已经unlock,在parkAndCheckInterrupt阻塞的线程开始了一下次的自旋,这里的current就是队列中的等待线程,所以c == 0。

由于之前上一个持有锁的线程unlock之后,unparkSuccessor通过CAS操作把哨兵节点的state设置为0,所以compareAndSetState(0, acquires) == true,并将当前线程设置为当前占用线程,方法将会返回true。

acquireQueued方法中p == head && tryAcquire(arg) == true,所以进入if代码块中:

  • 将当前线程节点设置为head,将thread和前置节点置空,相当于这个节点从阻塞到unlock竞争到锁之后,把这个节点变成了【新的哨兵节点】
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
  • 之前的哨兵节点通过 p.next == null,将没有任何引用,会被GC处理掉
  • failed = false,最后finally将不会取消排队。
  • 没有被打断过,interrupted = false,方法返回false。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值