AQS独占锁

58 阅读27分钟

前言

AQS(AbstractQueuedSynchronizer)是JAVA众多锁以及并发工具的基础,其底层采用乐观锁,大量使用了CAS操作, 并且在冲突时,采用自旋方式重试,以实现轻量级和高效地获取锁。

AQS虽然被定义为抽象类,但事实上它并不包含任何抽象方法。这是因为AQS是被设计来支持多种用途的,如果定义抽象方法,则子类在继承时必须要覆写所有的抽象方法,这显然是不合理的。所以AQS将一些需要子类覆写的方法都设计成protect方法,将其默认实现为抛出UnsupportedOperationException异常。如果子类使用到这些方法,但是没有覆写,则会抛出异常;如果子类没有使用到这些方法,则不需要做任何操作。

AQS中实现了锁的获取框架,锁的实际获取逻辑交由子类去实现,就锁的获取操作而言,子类必须重写 tryAcquire方法。

本篇我们将以ReentrantLock的公平锁为例来详细分析AQS获取和释放独占锁的流程。

本文中的源码基于JDK13 。

Example: FairSync in ReentrantLock

前面已经提到, AQS大多数情况下都是通过继承来使用的, 子类通过覆写 tryAcquire 来实现自己的获取锁的逻辑,我们这里以ReentrantLock为例来说明锁的获取流程。

值得注意的是, ReentrantLock有 公平锁 和 非公平锁 两种实现, 默认实现为非公平锁, 这体现在它的构造函数中:

public class ReentrantLock implements Lock, java.io.Serializable {
    /** 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 {
        ...
    }
    
    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync{
        ...
    }
    
    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        ...
    }
    
    /**
     * 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();
    }
    
    // 获取锁
    public void lock() {
        sync.acquire(1);
    }
    
    public void unlock() {
        sync.release(1);
    }
}

可以看出, FairSync 继承自 Sync, 而Sync继承自 AQS, ReentrantLock获取锁的逻辑是直接调用了 FairSync 或者 NonfairSync的逻辑.

lock 方法调用的 acquire方法来自父类AQS:

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这里首先给出完整的获取锁的流程图, 再逐行分析代码, 因为看源码的时候, 代码会在函数或者循环中来回跳转,读者可以对照以下流程图, 就不容易被绕晕了.

image.png

AQS三板斧

在开始看AQS源码之前,先来了解AQS三板斧:状态,队列,CAS。

  • 状态:state属性,它基本是整个工具的核心,锁的获取和释放都依赖于该状态。另外该状态是全局共享的,一般会被设置成volatile类型,以保证其修改的可见性;
  • 队列:队列是一个等待的集合,以链表的形式实现。队列采用的是悲观锁的思想,表示当前所等待的资源,状态或者条件短时间内可能无法满足。因此,它会将当前线程包装成某种类型的数据结构,扔到一个等待队列中,当一定条件满足后,再从等待队列中取出。
  • CAS: CAS操作是最轻量的并发处理,通常我们对于状态的修改都会用到CAS操作,因为状态可能被多个线程同时修改,CAS操作保证了同一个时刻,只有一个线程能修改成功,从而保证了线程安全,CAS操作基本是由Unsafe工具类的compareAndSwapXXX来实现的;CAS采用的是乐观锁的思想,因此常常伴随着自旋,如果发现当前无法成功地执行CAS,则不断重试,直到成功为止,自旋的的表现形式通常是一个死循环for(;;)。

状态

在AQS中,状态是由state属性来表示的,它是volatile类型的:

private volatile int state; 

该属性的值即表示锁的状态,state为0表示锁没有被占用,state大于0表示当前已经有线程持有该锁,这里之所以说大于0而不说等于1是因为可能存在可重入的情况。你可以把state变量当做是当前持有该锁的线程的重入次数【独占情况】或持有锁的线程数【共享情况】。

由于本篇我们分析的是独占锁,同一时刻,锁只能被一个线程所持有。通过state变量是否为0,我们可以分辨当前锁是否被占用,但光知道锁是不是被占用是不够的,我们并不知道占用锁的线程是哪一个。在AQS中,通过exclusiveOwnerThread属性来描述持有锁的线程:

private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer

exclusiveOwnerThread属性的值即为当前持有锁的线程。

队列

接着我们来看队列,AQS中,队列的实现是一个双向链表,被称为sync queue,它表示所有等待锁的线程的集合,类似于synchronized中的wait set。

AQS使用队列将当前线程包装成如下类型的数据结构扔到等待队列中:

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** 线程取消等待和争抢*/
    static final int CANCELLED =  1;
    /** 通知后继节点unpark */
    static final int SIGNAL    = -1;


    /** 用于表示为条件队列节点 */
    static final int CONDITION = -2;
    /**
     * 用于指示继续唤醒队列中的节点
     */
    static final int PROPAGATE = -3;

    // 等待状态
    volatile int waitStatus;

    //前驱和后驱节点
    volatile Node prev;

    volatile Node next;

    // 代表了等待的线程
    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;
    }
}

这个结构看起来很复杂,其实属性只有4类:

// 节点所代表的线程
volatile Thread thread;

// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用
volatile Node prev;
volatile Node next;

// 线程所处的等待锁的状态,初始化时,该值为0
volatile int waitStatus;
static final int CANCELLED =  1;
static final int SIGNAL    = -1;
static final int CONDITION = -2;//用于条件锁
static final int PROPAGATE = -3;//用于共享锁

// 该属性用于条件队列或者共享锁
Node nextWaiter;

注意,在这个Node类中也有一个状态变量waitStatus,它表示了当前Node所代表的线程的等待锁的状态,在独占锁模式下,我们只需要关注CANCELLED SIGNAL两种状态即可。这里还有一个nextWaiter属性,它在独占锁模式下永远为null,仅仅起到一个标记作用,没有实际意义。

说完队列中的节点,我们接着说回这个sync queue,AQS是怎么使用这个队列的呢,既然是双向链表,操纵它自然只需要一个头结点和一个尾节点:

// 头结点,不代表任何线程,是一个哑结点
private transient volatile Node head;

// 尾节点,每一个请求锁的线程会加到队尾
private transient volatile Node tail;

到这里,我们就了解到了这个sync queue的全貌:

image.png

head节点永远是一个哑结点(dummy node), 它不代表任何线程(某些情况下可以看做是代表了当前持有锁的线程),因此head所指向的Node的thread属性永远是null。只有从次头节点往后的所有节点才代表了所有等待锁的线程。也就是说,在当前线程没有抢到锁被包装成Node扔到队列中时,即使队列是空的,它也会排在第二个,我们会在它的前面新建一个dummy节点。

CAS操作

前面我们提到过,CAS操作大对数是用来改变状态的,在AQS中也不例外。

    /** CASes waitStatus field. */
    final boolean compareAndSetWaitStatus(int expect, int update) {
        return WAITSTATUS.compareAndSet(this, expect, update);
    }

    /** CASes next field. */
    final boolean compareAndSetNext(Node expect, Node update) {
        return NEXT.compareAndSet(this, expect, update);
    }

    /**
     * Atomically sets synchronization state to the given updated
     * value if the current state value equals the expected value.
     * This operation has memory semantics of a {@code volatile} read
     * and write.
     */
    protected final boolean compareAndSetState(int expect, int update) {
        return STATE.compareAndSet(this, expect, update);
    }

        /**
     * CASes tail field.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return TAIL.compareAndSet(this, expect, update);
    }

AQS独占锁核心属性总结

在进入正式的分析之前,我们先来总结下AQS核心属性:

  1. 锁相关的属性有两个:
private volatile int state; //锁的状态
private transient Thread exclusiveOwnerThread; // 当前持有锁的线程,注意这个属性是从AbstractOwnableSynchronizer继承而来
  1. sync queue相关的属性有两个:
private transient volatile Node head; // 队头,为dummy node
private transient volatile Node tail; // 队尾,新入队的节点
  1. 队列中的Node中需要关注的属性有三组:
// 节点所代表的线程
volatile Thread thread;

// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用
volatile Node prev;
volatile Node next;

// 线程所处的等待锁的状态,初始化时,该值为0
volatile int waitStatus;
static final int CANCELLED =  1;
static final int SIGNAL    = -1;

争抢独占锁

acquire

acquire 定义在AQS类中,描述了获取锁的流程:

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

可以看出, 该方法中涉及了四个方法的调用:

(1)tryAcquire(arg)

该方法由继承AQS的子类实现, 为获取锁的具体逻辑。

(2)addWaiter(Node mode)

该方法由AQS实现, 负责在获取锁失败后调用, 将当前请求锁的线程包装成Node扔到sync queue中去,并返回这个Node。

(3)acquireQueued(final Node node, int arg)

该方法由AQS实现,这个方法比较复杂, 主要对上面刚加入队列的Node不断尝试以下两种操作之一:

  • 在前驱节点就是head节点的时候,继续尝试获取锁
  • 将当前线程挂起,使CPU不再调度它

(4)selfInterrupt

该方法由AQS实现, 用于中断当前线程。由于在整个抢锁过程中,线程都是不响应中断的。若在抢锁的过程中发生了中断,AQS将通过一个字段记录是否发生过中断,如果返回的时候发现曾经发生过中断,则在退出acquire方法之前,就调用selfInterrupt自我中断一下。

除了获取锁的逻辑 tryAcquire(arg)由子类实现外, 其余方法均由AQS实现。

接下来我们重点来看 FairSync 所实现的获取锁的逻辑:

tryAcquire

tryAcquire 获取锁的逻辑其实很简单,判断当前锁有没有被占用:

  • 如果锁没有被占用, 尝试以公平的方式获取锁
  • 如果锁已经被占用, 检查是不是锁重入 获取锁成功返回true, 失败则返回false
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 首先获取当前锁的状态
    int c = getState(); 
    
    // c=0 说明当前锁是avaiable的, 没有被任何线程占用, 可以尝试获取
    // 因为是实现公平锁, 所以在抢占之前首先看看队列中有没有排在自己前面的Node
    // 如果没有人在排队, 则通过CAS方式获取锁, 就可以直接退出了
    if (c == 0) {
        if (!hasQueuedPredecessors() 
        && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current); // 将当前线程设置为占用锁的线程
            return true;
        }
    }
    
    // 如果 c>0 说明锁已经被占用了
    // 对于可重入锁, 这个时候检查占用锁的线程是不是就是当前线程,是的话,说明已经拿到了锁, 直接重入就行
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
       
        setState(nextc);
        /* 为什么不CAS?
        setState方法如下:
        protected final void setState(int newState) {
            state = newState;
        }
        */
        return true;
    }
    
    // 到这里说明有人占用了锁, 并且占用锁的不是当前线程, 则获取锁失败
    return false;
}

从这里可以看出,获取锁其实主要就是干一件事: 将state的状态通过CAS操作由0改写成1

由于是CAS操作,必然是只有一个线程能执行成功。则执行成功的线程即获取了锁,在这之后,才有权利将exclusiveOwnerThread的值设成自己。 另外对于可重入锁,如果当前线程已经是获取了锁的线程了,它还要注意增加锁的重入次数。

值得一提的是,这里修改state状态的操作,一个用了CAS方法compareAndSetState,一个用了普通的setState方法。这是因为用CAS操作时,当前线程还没有获得锁,所以可能存在多线程同时在竞争锁的情况;而调用setState方法时,是在当前线程已经是持有锁的情况下,因此对state的修改是安全的,只需要普通的方法就可以了。

回到调用的地方:

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

addWaiter

如果执行到此方法, 说明前面尝试获取锁的tryAcquire已经失败了, 既然获取锁已经失败了, 就要将当前线程包装成Node,加到等待锁的队列中去, 因为是FIFO队列, 所以自然是直接加在队尾。 方法调用为:

addWaiter(Node.EXCLUSIVE)

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(mode);
        /**
            Node(Node nextWaiter) {
                THREAD.set(this, Thread.currentThread());
            }
        */

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                // 如果队列不为空, 则用CAS方式将当前节点设为尾节点
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }

在这个方法中,我们首先会尝试直接入队,但是因为目前是在并发条件下,所以有可能同一时刻,有多个线程都在尝试入队,导致compareAndSetTail(pred, node)操作失败——因为有可能其他线程已经成为了新的尾节点,导致尾节点不再是我们之前看到的那个了。

如果入队失败了,将通过自旋+CAS的方式,确保当前节点入队。

尾分叉

在继续往下之前,我们先分析一个比较有趣的现象,尾分叉。 将一个节点node添加到sync queue的末尾也需要三步:

  • 设置node的前驱节点为当前的尾节点:node.prev = t
  • 修改tail属性,使它指向当前节点(同一时刻仅有一个线程会成功)
  • 修改原来的尾节点,使它的next指向当前节点

image.png

但是需要注意的,这里的三步并不是一个原子操作,第一步很容易成功;而第二步由于是一个CAS操作,在并发条件下有可能失败,第三步只有在第二步成功的条件下才执行。这里的CAS保证了同一时刻只有一个节点能成为尾节点,其他节点将失败,失败后将回到for循环中继续重试。

所以,当有大量的线程在同时入队的时候,同一时刻,只有一个线程能完整地完成这三步,而其他线程只能完成第一步,于是就出现了尾分叉:

image.png 注意,这里第三步是在第二步执行成功后才执行的,这就意味着,有可能即使我们已经完成了第二步,将新的节点设置成了尾节点,此时原来旧的尾节点的next值可能还是null(因为还没有来的及执行第三步),所以如果此时有线程恰巧从头节点开始向后遍历整个链表,则它是遍历不到新加进来的尾节点的,但是这显然是不合理的,因为现在的tail已经指向了新的尾节点。

另一方面,当我们完成了第二步之后,第一步一定是完成了的,所以如果我们从尾节点开始向前遍历,已经可以遍历到所有的节点。这也就是为什么我们在AQS相关的代码中,有时候常常会出现从尾节点开始逆向遍历链表——因为一个节点要能入队,则它的prev属性一定是有值的,但是它的next属性可能暂时还没有值。

至于那些“分叉”的入队失败的其他节点,在下一轮的循环中,它们的prev属性会重新指向新的尾节点,继续尝试新的CAS操作,最终,所有节点都会通过自旋不断的尝试入队,直到成功为止。

addWaiter总结

至此,我们就完成了addWaiter(Node.EXCLUSIVE)方法的完整的分析,该方法并不设计到任何关于锁的操作,它就是解决了并发条件下的节点入队问题。具体来说就是该方法保证了将当前线程包装成Node节点加入到等待队列的队尾,如果队列为空,则会新建一个哑节点作为头节点,再将当前节点接在头节点的后面。

addWaiter(Node.EXCLUSIVE)方法最终返回了代表了当前线程的Node节点,在返回的那一刻,这个节点必然是当时的sync queue的尾节点。

我们再回到获取锁的逻辑中:

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

当addWaiter(Node.EXCLUSIVE)执行完毕后,节点现在已经被成功添加到sync queue中了,接下来将执行acquireQueued方法。

acquireQueued

该方法是最复杂的一个方法, 在分析之前首先简单的说明几点:

(1) 能执行到该方法, 说明addWaiter 方法已经成功将包装了当前Thread的节点添加到了等待队列的队尾; (2) 该方法将根据条件再次尝试去获取锁; (3) 在尝试获取锁失败后, 判断是否需要把当前线程挂起。

什么条件下会再次尝试获取锁呢?

当前节点的前驱节点就是HEAD节点

如果当前节点的前驱节点就是head节点,那就说明当前节点已经是排在整个等待队列最前面的了,在下一时刻很有可能获取到锁。

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) {
                // 获取当前节点前驱节点并判断是不是head
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                // 判断具不具备park的条件,具备则休眠,不具备循环
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

注意,这里又来了个自旋操作,我们一段段来看:

final Node p = node.predecessor();
// 在当前节点的前驱就是HEAD节点时, 再次尝试获取锁
if (p == head && tryAcquire(arg)) {
    setHead(node);
    p.next = null; // help GC
    return interrupted;
}

首先获取当前节点的前驱节点; 如果前驱节点就是head节点,那说明当前线程已经排在了队列的最前面,所以这里我们再试着去获取锁。如果这一次获取成功了,即tryAcquire方法返回了true, 则我们将进入if代码块,调用setHead方法:

这个方法将head指向传进来的node,并且将node的thread和prev属性置为null, 如下图所示:

image.png 可以看出,这个方法的本质是丢弃原来的head,将head指向已经获得了锁的node。但是接着又将该node的thread属性置为null了,这某种意义上导致了这个新的head节点又成为了一个哑节点,它不代表任何线程。这某种程度上就是将当前线程从等待队列里面拿出来了,是一个变相的出队操作

image.png 接下来我们再来看看另一种情况,即p == head && tryAcquire(arg)返回了false,此时我们需要判断是否需要将当前线程挂起:

 // 判断具不具备park的条件,具备则休眠,不具备循环
if (shouldParkAfterFailedAcquire(p, node))
      interrupted |= parkAndCheckInterrupt();

shouldParkAfterFailedAcquire

从函数名也可以看出, 该方法用于判断在获取锁失败后, 是否将线程挂起.

决定的依据就是当前节点的前驱节点的waitStatus值

我们先来回顾一下独占锁相关的waitStatus有哪些状态值:

static final int CANCELLED =  1;   //取消
static final int SIGNAL    = -1;	//通知后面节点

另外,每一个节点最开始的时候waitStatus的值都被初始化为0。

那么CANCELLED和SIGNAL代表什么意思呢?

  • CANCELLED状态很好理解,它表示Node所代表的当前线程已经取消了排队,即放弃获取锁了。

  • SIGNAL:不是表征当前节点的状态,而是当前节点的下一个节点的状态。 当一个节点的waitStatus被置为SIGNAL,就说明它的下一个节点已经被挂起了(或者马上就要被挂起了),因此在当前节点释放了锁或者放弃获取锁时,如果它的waitStatus属性为SIGNAL,它还要完成一个额外的操作——唤醒它的后继节点。

因此,SIGNAL这个状态的设置常常不是节点自己给自己设的,而是后继节点设置的。 再来看看shouldParkAfterFailedAcquire是怎么用的:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 获得前驱节点的ws
    if (ws == Node.SIGNAL)
        // 前驱节点的状态已经是SIGNAL了,说明闹钟已经设了,可以直接睡了
        return true;
    if (ws > 0) {
        // 当前节点的 ws > 0, 则为 Node.CANCELLED 说明前驱节点已经取消了等待锁(由于超时或者中断等原因)
        // 既然前驱节点取消了, 那就继续往前找, 直到找到一个还在等待锁的节点
        // 然后我们跨过这些不等待锁的节点, 直接排在等待锁的节点的后面
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 前驱节点的状态既不是SIGNAL,也不是CANCELLED,独占情况下只会为0
        // 用CAS设置前驱节点的ws为 Node.SIGNAL,给自己定一个闹钟
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

可以看出,shouldParkAfterFailedAcquire所做的事情无外乎:

  • 如果为前驱节点的waitStatus值为 Node.SIGNAL 则直接返回 true
  • 如果为前驱节点的waitStatus值为 Node.CANCELLED (ws > 0), 则跳过那些节点, 重新寻找正常等待中的前驱节点,然后排在它后面,返回false
  • 其他情况, 将前驱节点的状态改为 Node.SIGNAL, 返回false

NOTE: 这个函数只有在当前节点的前驱节点的waitStatus状态本身就是SIGNAL的时候才会返回true, 其他时候都会返回false, 我们再回到这个方法的调用处:

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))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
}

可以看出,当shouldParkAfterFailedAcquire返回false后,会继续回到循环中再次尝试获取锁——这是因为前驱节点可能变化了,存在为头结点的可能性。

当shouldParkAfterFailedAcquire返回true,即当前节点的前驱节点的waitStatus状态已经设为SIGNAL后,此时就调用parkAndCheckInterrupt将当前线程挂起了:

parkAndCheckInterrupt

到这个函数已经是最后一步了, 就是将线程挂起, 等待被唤醒

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 线程被挂起,停在这里不再往下执行了
    return Thread.interrupted();
}

NOTE:LockSupport.park(this)执行完成后线程就被挂起了,除非其他线程unpark了当前线程,或者当前线程被中断了,否则代码是不会再往下执行的,后面的Thread.interrupted()也不会被执行(用于记录线程是否被中断了)。

image.png

Example: ReentrantLock的锁释放

释放独占锁

由于锁的释放操作对于公平锁和非公平锁都是一样的, 所以, unlock的逻辑并没有放在 FairSync 或 NonfairSync 里面, 而是直接定义在 ReentrantLock类中:

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

release

release方法定义在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;
}

可以看出, 相比获取锁的acquire方法, 释放锁的过程要简单很多, 它只涉及到两个子函数的调用:

  • tryRelease(arg):该方法由继承AQS的子类实现, 为释放锁的具体逻辑;

  • unparkSuccessor(h):唤醒后继线程。

tryRelease

tryRelease方法由ReentrantLock的静态类Sync实现:

NOTE:能执行到释放锁的线程, 一定是已经获取了锁的线程。

相比获取锁的操作, 这里没有使用任何CAS操作, 也是因为当前线程已经持有了锁, 所以可以直接安全的操作, 不会产生竞争.

protected final boolean tryRelease(int releases) {
    
    // 首先将当前持有锁的线程个数减1(回溯到调用源头sync.release(1)可知, releases的值为1)
    // 这里的操作主要是针对可重入锁的情况下, c可能大于1
    int c = getState() - releases; 
    
    // 释放锁的线程当前必须是持有锁的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    
    // 如果c为0了, 说明锁已经完全释放了
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

unparkSuccessor

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

锁成功释放之后, 接下来就是判断是否唤醒后继节点了, 这个方法同样定义在AQS中.

首先分析进入unparkSuccessor的条件,:

h != null && h.waitStatus != 0

h!=null 我们容易理解, h.waitStatus != 0是个什么意思呢?

逆向思考: 给waitStatus赋值过的地方有两处:

  • shouldParkAfterFailedAcquire 函数中将前驱节点的 waitStatus设为Node.SIGNAL;
  • 新建一个节点的时候, 在addWaiter 函数中, 当我们将一个新的节点添加进队列或者初始化空队列的时候, 都会新建节点 而新建的节点的waitStatus在没有赋值的情况下都会初始化为0.

所以当一个head节点的waitStatus为0说明什么呢, 说明这个head节点后面没有在挂起等待中的后继节点了(如果有的话, head的ws就会被后继节点设为Node.SIGNAL了), 自然也就不要执行 unparkSuccessor 操作了.

接下来我们就看看unparkSuccessor的代码:

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    
    // 如果head节点的ws比0小, 则直接将它设为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 通常情况下, 要唤醒的节点就是自己的后继节点
    // 如果后继节点存在且也在等待锁, 那就直接唤醒它
    // 但是有可能存在 后继节点取消等待锁 的情况
    // 此时从尾节点开始向前找起, 直到找到距离head节点最近的非取消状态的节点
    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; // 注意! 这里找到了之并有return, 而是继续向前找
    }
    // 如果找到了还在等待锁的节点,则唤醒它
    if (s != null)
        LockSupport.unpark(s.thread);
}

QUESTION: 为什么要从尾节点开始逆向查找, 而不是直接从head节点往后正向查找, 这样只要正向找到第一个, 不就可以停止查找了吗?

首先我们要看到,从后往前找是基于一定条件的:

if (s == null || s.waitStatus > 0)

后继节点不存在,或者后继节点取消了排队,这一条件大多数条件下是不满足的。 因为虽然后继节点取消排队很正常,但是节点在挂起前,都会给自己找一个waitStatus状态为SIGNAL的前驱节点,而跳过那些已经cancel掉的节点。

所以,这个从后往前找的目的其实是为了照顾刚刚加入到队列中的节点,从前往后可能找不到新加入的节点。

所以总结来说,之所以从后往前遍历是因为,在多线程并发的条件下的,如果一个节点的next属性为null, 并不能保证它就是尾节点(可能是因为新加的尾节点还没来得及执行pred.next = node), 但是一个节点如果能入队, 则它的prev属性一定是有值的,所以反向查找一定是最精确的。

最后, 在调用了 LockSupport.unpark(s.thread) 也就是唤醒了线程。

回到线程挂起的地方:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); //  唤醒之后就能继续往下执行了
    return Thread.interrupted();
}

线程从这里唤醒了,他将调用 Thread.interrupted()并返回。

Thread.interrupted()这个函数将返回当前正在执行的线程的中断状态,并清除它。接着,我们再返回到parkAndCheckInterrupt被调用的地方:

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))
                interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
}

可见,如果Thread.interrupted()返回true,则 parkAndCheckInterrupt()就返回true, if条件成立,interrupted状态将设为true; 如果Thread.interrupted()返回false, 则 interrupted 仍为false。

再接下来我们又回到了for (;;) 死循环的开头,进行新一轮的抢锁。

假设这次我们抢到了,我们将从 return interrupted处返回,返回到哪里呢? 当然是acquireQueued的调用处啦:

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

如果acquireQueued的返回值为true, 我们将执行 selfInterrupt():

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

而它的作用,就是中断当前线程。

为什么要中断线程?

如果发现当前线程曾经被中断过,那就把当前线程再中断一次,将这个中断补上。

注意,中断对线程来说只是一个建议,一个线程被中断只是其中断状态被设为true, 线程可以选择忽略这个中断,中断一个线程并不会影响线程的执行。

思考

1、节点的SIGNAL状态只是作为AQS的优化项,在不设置时也可以正常工作;

2、等待队列的双向链表也为优化项,实际上可为单向链表实现;

3、AQS的线程存在被额外unpark的可能性,导致代码运行出现不可预料的结果【存疑】。

总结

  1. AQS中用state属性表示锁,如果能成功将state属性通过CAS操作从0设置成1即获取了锁;
  2. 获取了锁的线程才能将exclusiveOwnerThread设置成自己;
  3. addWaiter负责将当前等待锁的线程包装成Node,并成功地添加到队列的末尾, 通过cas和自旋完成;
  4. acquireQueued方法用于在Node成功入队后,继续尝试获取锁(取决于Node的前驱节点是不是head),或者将线程挂起;
  5. shouldParkAfterFailedAcquire方法用于保证当前线程的前驱节点的waitStatus属性值为SIGNAL,从而保证了自己挂起后,前驱节点会负责在合适的时候唤醒自己;
  6. parkAndCheckInterrupt方法用于挂起当前线程,并检查中断状态;
  7. 如果最终成功获取了锁,线程会从lock()方法返回,继续往下执行;否则,线程会阻塞等待;
  8. tryRelease函数用于释放锁;
  9. unparkSuccesser用于唤醒后继节点。

参考

  1. segmentfault.com/a/119000001…
  2. segmentfault.com/a/119000001…
  3. www.modb.pro/db/566138(设…
  4. www.cnblogs.com/micrari/p/6…