java并发编程之AQS

手撕AQS源码(1) – 独占锁的获取



前言

并发编程是Java的又一重要的基本功,本篇主要讲解Java层面实现的锁,例如ReentrantLock等,而这些类都是基于AQS实现的,所以搞懂AQS非常有必要,下面开始手撕源码之旅吧


一、AQS是什么?

全称是 AbstractQueuedSynchronizer,是一个用于构建锁和同步器的框架,其底层采用乐观锁,大量使用了CAS操作, 并且在冲突时,采用自旋方式重试,以实现轻量级和高效地获取锁。

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

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

本篇我们将以ReentrantLock的公平锁为例来详细看看使用AQS获取独占锁的流程。

本文中的源码基于JDK1.8 。为了篇幅考虑, 源码中的长篇注释就不贴了,强烈推荐大家读一读AQS中的注释,例如:AQS类注释中就告诉了我们如何自定义一个锁


二、Java并发工具类的三板斧

在开始看AQS源码之前,我们先来了解以下java并发工具的设计套路,我把它总结成三板斧:

状态,队列,CAS
每当我们学习一个java并发编程工具的时候,我们首先要抓住这三点。

1.状态

一般是一个state属性,它基本是整个工具的核心,通常整个工具都是在设置和修改状态,很多方法的操作都依赖于当前状态是什么。由于状态是全局共享的,一般会被设置成volatile类型,以保证其修改的可见性;

2.队列

队列通常是一个等待的集合,大多数以链表的形式实现。队列采用的是悲观锁的思想,表示当前所等待的资源,状态或者条件短时间内可能无法满足。因此,它会将当前线程包装成某种类型的数据结构,扔到一个等待队列中,当一定条件满足后,再从等待队列中取出。

3.CAS

CAS操作是最轻量的并发处理,通常我们对于状态的修改都会用到CAS操作,因为状态可能被多个线程同时修改,CAS操作保证了同一个时刻,只有一个线程能修改成功,从而保证了线程安全,CAS操作基本是由Unsafe工具类的compareAndSwapXXX来实现的;CAS采用的是乐观锁的思想,因此常常伴随着自旋,如果发现当前无法成功地执行CAS,则不断重试,直到成功为止,自旋的的表现形式通常是一个死循环for(;😉。


三 AQS核心实现

上面我们已经总结了java并发编程的套路,下面我们就以这个套路为切入点来分析AQS的实现。

状态

private volatile int state;

该属性的值即表示了锁的状态,state为0表示锁没有被占用,state大于0表示当前已经有线程持有该锁,这里之所以说大于0而不说等于1是因为可能存在可重入的情况。你可以把state变量当做是当前持有该锁的线程数量, 子类需要定义如何维护这个状态,控制如何获取锁和释放锁. 并提供了一些方法:
getState - 获取 state 状态
setState - 设置 state 状态
compareAndSetState - cas 机制设置 state 状态

Owner

    private transient Thread exclusiveOwnerThread;

这个成员变量继承自AbstractOwnableSynchronizer,
由于本篇我们分析的是独占锁,同一时刻,锁只能被一个线程所持有。通过state变量是否为0,我们可以分辨当前锁是否被占用,但光知道锁是不是被占用是不够的,我们并不知道占用锁的线程是哪一个。在监视器锁中,我们用ObjectMonitor对象的_owner属性记录了当前拥有监视器锁的线程,而在AQS中,我们将通过exclusiveOwnerThread属性来表示是哪个线程持有了锁

队列

接着我们来看队列,AQS中队列的实现是一个双向链表,被称为sync queue,它表示所有等待锁的线程的集合,是一个基于 FIFO 的等待队列, 有点类似于我们前面介绍synchronized原理的时候说的wait set。

我们前面说过,在并发编程中使用队列通常是将当前线程包装成某种类型的数据结构扔到等待队列中,我们先来看看队列中的每一个节点是怎么个结构:

    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;//这个标识当前锁是独占锁,即独占锁时,node节点的nextWaiter属性为null

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        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;
        
        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;
        }
    }

分析:

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;

waitStatus: 当前Node所代表的线程等待锁的状态在独占锁模式下,我们只需要关注CANCELLED SIGNAL两种状态即可
4.

// 该属性用于条件队列或者共享锁
Node nextWaiter;
static final Node EXCLUSIVE = null;
static final Node EXCLUSIVE = null;

它在独占锁模式下永远为null,仅仅起到一个标记作用,没有实际意义

sync queue

AQS是如何使用这个Queue的呢?操纵双向链表需要一个头结点,一个尾结点

 private transient volatile Node head;//头结点

 private transient volatile Node tail;//尾结点

sync queue的图示如下:
在这里插入图片描述
不过这里有一点我们提前说一下,在AQS中的队列是一个CLH队列,它的head节点永远是一个哑结点(dummy node), 它不代表任何线程(某些情况下可以看做是代表了当前持有锁的线程),因此head所指向的Node的thread属性永远是null。只有从次头节点往后的所有节点才代表了所有等待锁的线程。也就是说,在当前线程没有抢到锁被包装成Node扔到队列中时,即使队列是空的,它也会排在第二个,我们会在它的前面新建一个dummy节点(具体的代码我们在后面分析源码时再详细讲)。为了便于描述,下文中我们把除去head节点的队列称作是等待队列,在这个队列中的节点才代表了所有等待锁的线程:在这里插入图片描述
总结一下Node节点各个参数的含义:

  • thread:表示当前Node所代表的线程
  • waitStatus:表示节点所处的等待状态,共享锁模式下只需关注三种状态:SIGNAL CANCELLED 初始态(0)
  • prev next:节点的前驱和后继
  • nextWaiter:进作为标记,值永远为null,表示当前处于独占锁模式

CAS操作

前面我们提到过,CAS操作大对数是用来改变状态的,在AQS中也不例外。我们一般在静态代码块中初始化需要CAS操作的属性的偏移量:(这段代码在AQS类的底部)

    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;

    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }

从这个静态代码块中我们也可以看出,CAS操作主要针对5个属性,包括AQS的3个属性state,head和tail, 以及Node对象的两个属性waitStatus,next。说明这5个属性基本是会被多个线程同时访问的。

定义完属性的偏移量之后,接下来就是CAS操作本身了:

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) {
    return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
}
private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
    return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}

总结

AQS的核心属性:

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

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

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

四 举例分析:FairSync in ReentrantLock

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

值得注意的是, ReentrantLock有 公平锁 和 非公平锁 两种实现, 默认实现为非公平锁

加锁流程分析

ReentrantLock的lock()

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

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

Sync是ReentrantLock实现锁的基础,有NonfairSync 和FairSync 两个子类,而Sync又继承与AQS,这里和之前说过的AQS是各种锁实现的基础,要用AQS,就要弄一个子类继承AQS,然后实现AQS中protect的方法,即那些加锁,解锁的方法
Sync及其子类关系
此处我们以FairSync为例分析, 点进FairSync的lock()

final void lock() {
            acquire(1);
        }

一步一步的追,点进acquire(1),发现调用的其实是AQS的acquire()

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

大神写的代码就是这么滴简洁啊,短短3行,却有几百行的复杂逻辑在里面,下面正片开始
先来tryAcquire(1),这个tryAcquire()是要子类去实现的,此处我们找到FairSync的tryAcquire()

tryAcquire

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//c为当前锁的状态
            
//如果c==0,表示当前锁是可获得的,没有被任何线程占用,可以尝试获取
//至于为什么==0就是没被占用,请往下看,莫急,show you the code
            if (c == 0) {
//hasQueuedPredecessors()和compareAndSetState(0, acquires)源码已在下方贴出
//如果有前驱元素,则说明还轮不到自己来抢锁,则直接返回抢锁失败
//如果没有前驱元素,自己就是在head后面的那个元素,然后去看compareAndSetState(0, acquires),cas更新state为1是否成功,
//如果成功,则将持有锁的线程设置为当前线程,并返回加锁成功
//如果失败,返回加锁失败
              if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果当前线程就是持有锁的那个线程,说明发生了锁重入,state+1,然后setState,加锁成功
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
  • hasQueuedPredecessors(): 判断队列中有没有排在自己前面的Node
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
  • compareAndSetState(0, acquires): cas操作更新state
protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
总结
  • 获取锁其实主要就是干一件事:将state的状态通过CAS操作由0改写成1
  • 在cas 设置state为1失败时,会返回加锁失败,其他返回加锁成功
两个小疑问
  • 同样都是set state为1,为什么第一个if用cas,而第二个if else中没有用cas?
    解答: 这是因为用CAS操作时,当前线程还没有获得锁,所以可能存在多线程同时在竞争锁的情况;而调用setState方法时,是在当前线程已经是持有锁的情况下,因此对state的修改是安全的,只需要普通的方法就可以了
  • hasQueuedPredecessors()为什么要调用这个方法看下前面有没有元素呢?
    解答: 因为这里是公平锁的实现,所以要看下队列中前面是否有等待的thread(Node),如果没有,那我就是阻塞队列中的第一个节点,该我抢锁; 如果有,那我就没资格抢锁,我应该去它后面排队啊,莫急,后面代码实现了这一点

那么继续我们的手撕之旅吧

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

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

tryAcquire已经分析完毕,当cas抢锁失败后,会再去执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
好家伙,我直接好家伙,先来分析addWaiter(Node.EXCLUSIVE)
首先说这个方法的含义是: 将cas抢锁失败的thread包装成Node,放入队列中,并返回新构建的Node
下面开始逐行手撕

private Node addWaiter(Node mode) {//node为null,标识独占锁
//构造一个包含当前线程的新的Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
//这个判断很重要,如果尾结点为null,说明此时队列是空的,要执行下面的enq()初始化队列
        if (pred != null) {
            node.prev = pred;//将新构建的Node节点的前驱指针指向之前的尾结点
            if (compareAndSetTail(pred, node)) {//cas将包含当前线程的node加入到尾结点
                pred.next = node;//如果成功,将之前尾结点的后继指针指向新构建的Node,失败执行enq()
                return node;
            }
        }
//执行到这1.队列为null(几率不大),2.加入队尾时cas失败
//为了阅读方法,enq(node)再开一行单独讲解
        enq(node);
        return node;
    }
小总结
  • 独占模式下的Node中的nextWaiter都是null
  • head节点是一个哑节点,即thread为null
  • 队列是延时初始化的
思考

将节点接入队列队尾为什么也要使用cas?

  • 因为目前是在并发条件下,所以有可能同一时刻,有多个线程都在尝试入队,导致compareAndSetTail(pred, node)操作失败——因为有可能其他线程已经成为了新的尾节点,导致尾节点不再是我们之前看到的那个pred了。
enq(node): 此方法的目的是通过自旋+CAS的方式,确保当前节点入队。
private Node enq(final Node node) {//node是包含了当前线程的新建节点
        for (;;) {
            Node t = tail;
            //第一次会进第一个if,因为就是tail为null才走到这里的,但是当下一次循环时,就会进入else中了,因为tail不是null了
            if (t == null) { // t==null说明队列为空,会进入if,初始化一个空节点,即没有包含thread的结点
                if (compareAndSetHead(new Node()))
                    tail = head;//尾节点指向当前的头节点,然后进入下一轮循环。
            } else {//下一轮循环中,尾节点已经不为null了,此时再将我们包装了当前线程的Node加到这个空节点后面。
                node.prev = t;//这就意味着,在这个等待队列中,头结点是一个“哑节点”,它不代表任何等待的线程。
                if (compareAndSetTail(t, node)) {//head节点不代表任何线程,它就是一个空节点!!!
                    t.next = node;
                    return t;
                }
            }
        }
    }
思考

这段代码有什么问题么?

  • Doug Lea大神写的,怎么可能有问题呢, 是你有问题吧!
    这段自旋代码是在多线程场景下的, 在else代码中
else {
    node.prev = t;//多线程
    if (compareAndSetTail(t, node)) {
        t.next = node;//运行到这,才是单线程
        return t;
    }
}

node.prev = t;这行代码是在多线程环境下运行的,这不是意味着有好几个Node的前驱指针指向尾结点
当有大量的线程在同时入队的时候,同一时刻,只有一个线程能完整地完成这三步,而其他线程只能完成第一步,于是就出现了尾分叉:
在这里插入图片描述
这个现象也决定了后面解锁的一段代码,所以理解这个东西很有必要:

  • 这里第三步是在第二步执行成功后才执行的,这就意味着,有可能即使我们已经完成了第二步,将新的节点设置成了尾节点,此时原来旧的尾节点的next值可能还是null(因为还没有来的及执行第三步),所以如果此时有线程恰巧从头节点开始向后遍历整个链表,则它是遍历不到新加进来的尾节点的,但是这显然是不合理的,因为现在的tail已经指向了新的尾节点。
  • 另一方面,当我们完成了第二步之后,第一步一定是完成了的,所以如果我们从尾节点开始向前遍历,已经可以遍历到所有的节点。这也就是为什么我们在AQS相关的源码中,有时候常常会出现从尾节点开始逆向遍历链表——因为一个节点要能入队,则它的prev属性一定是有值的,但是它的next属性可能暂时还没有值。
  • 至于那些“分叉”的入队失败的其他节点,在下一轮的循环中,它们的prev属性会重新指向新的尾节点,继续尝试新的CAS操作,最终,所有节点都会通过自旋不断的尝试入队,直到成功为止。
addWaiter总结

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

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

不过值得注意的是,enq方法也是有返回值(虽然这里我们并没有使用它的返回值),但是它返回的是node节点的前驱节点,这个返回值虽然在addWaiter方法中并没有使用,但是在其他地方会被用到。

下面思绪回到获取锁的逻辑,继续干

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
acquireQueued(final Node node, int arg)
final boolean acquireQueued(final Node node, int arg) {//node是新加入队尾的节点,arg是1
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//拿到新加入节点的前一个节点
                if (p == head && tryAcquire(arg)) {//如果是head,则再尝试获取锁
                    setHead(node);//1.此时为啥不用cas来设置,因为tryAcquire是cas操作成功才会走这一步,此时只有一个线程
                    p.next = null; // help GC //2.setHead(node)中为什么要将head指向当前节点,因为tryAcquire()成功时已经记录当前成功获取锁的线程了,此时相当于让当前节点出队
                    failed = false; //
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&//请看点进shouldParkAfterFailedAcquire()看解析
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

分段手撕

				final Node p = node.predecessor();//拿到新加入节点的前一个节点
                if (p == head && tryAcquire(arg)) {//如果前驱节点是head,则再尝试获取锁
                    setHead(node);// 如果获取锁成功,则设置head,详解见下方
                    p.next = null; // help GC,将node的前驱节点的后继指针断开,方便前驱节点的回收,细啊
                    failed = false; //额,这行代码感觉可以拿掉
                    return interrupted;//如果当前线程是在head后面的即优先级最高的节点,且抢锁成功,那么返回false
                }

setHead(node)

//这段代码含义: 将head指向刚创建的node,并将node的thread和前驱指针置位null
//这某种意义上导致了这个新的head节点又成为了一个哑节点,它不代表任何线程。为什么要这样做呢,
//因为在tryAcquire调用成功后,exclusiveOwnerThread属性就已经记录了当前获取锁的线程了,
//此处没有必要再记录。这某种程度上就是将当前线程从等待队列里面拿出来了,是一个变相的出队操作。
private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
思考
 if (p == head && tryAcquire(arg)) {//如果前驱节点是head,则再尝试获取锁
                    setHead(node);

此时为啥不用cas来设置?

  • 因为tryAcquire是cas操作成功才会走这一步,此时只有一个线程

继续吧,如果tryAcquire()失败,即抢锁失败,执行

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
                    interrupted = true;
}

继续逐个分析:

shouldParkAfterFailedAcquire()
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//pred前驱节点,node当前节点
        int ws = pred.waitStatus;
//如果前驱节点的waitStatus是-1了,那么可以安心去执行park了,即前驱节点会在自己出队的时候叫醒这一个节点
        if (ws == Node.SIGNAL)
            return true;//返回可以park线程
//如果前驱节点的waitStatus>0即为Node.CANCELLED,则说明前驱节点已经取消了等待(由于超时或者中断等原因)
        if (ws > 0) {
            do {
            //一直往前找,直到找到一个前驱节点的waitStatus是-1
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;//然后直接排在等待锁的节点的后面
        } else {
//如果前驱节点的waitStatus为-1, 用CAS设置前驱节点的ws为 Node.SIGNAL,给自己定一个闹钟
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;//返回不应该park,不要忘记这段代码是在for(,,)中,返回false会继续下次自旋
    }

从方法名中就可以看出含义: 当抢锁失败后是否应该park线程,决定的依据就是前驱节点的waitStatus值。
先来回顾一下waitStatus有哪些状态值:

static final int CANCELLED =  1;
static final int SIGNAL    = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;

一共有四种状态,但是我们在开篇的时候就说过,在独占锁锁的获取操作中,我们只用到了其中的两个——CANCELLED和SIGNAL。
当然,前面我们在创建节点的时候并没有给waitStatus赋值,因此每一个节点最开始的时候waitStatus的值都被初始化为0,即不属于上面任何一种状态。

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

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

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

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

  • 如果为前驱节点的waitStatus值为 Node.SIGNAL 则直接返回 true
  • 如果为前驱节点的waitStatus值为 Node.CANCELLED (ws > 0), 则跳过那些节点, 重新寻找正常等待中的前驱节点,然后排在它后面,返回false
  • 其他情况, 将前驱节点的状态改为 Node.SIGNAL, 返回false
    注意了,这个函数只有在当前节点的前驱节点的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) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

即是在for循环中的,所以当shouldParkAfterFailedAcquire()返回false,会继续下一次循环,直到找到node的前驱节点的waitStatus==-1的情况,才会返回true,然后执行parkAndCheckInterrupt()

parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//线程走到这就被挂起了,下面的return不会走的
        return Thread.interrupted();
}

也就是说当把node放到前驱节点的waitStatus为-1的节点的后面,放好了后, 开始执行park(),线程被挂起,除非其他线程unpark了当前线程,或者当前线程被中断了,否则代码是不会再往下执行的,后面的Thread.interrupted()也不会被执行,那后面这个Thread.interrupted()是干什么用的呢?
这个跟释放锁的代码有关,且听下回分解

思考
  • 在独占锁模式下,waitStatus的值有-1和1,那么else的代码会走吗?
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);
        }
  • waitStatus是在什么情况下被设置为1,即取消等待的呢?

总结

  • AQS中用state属性表示锁,如果能成功将state属性通过CAS操作从0设置成1即获取了锁
  • 获取了锁的线程才能将exclusiveOwnerThread设置成自己
  • addWaiter负责将当前等待锁的线程包装成Node,并成功地添加到队列的末尾,这一点是由它调用的enq方法保证的,enq方法同时还负责在队列为空时初始化队列。
  • acquireQueued方法用于在Node成功入队后,继续尝试获取锁(取决于Node的前驱节点是不是head),或者将线程挂起
  • shouldParkAfterFailedAcquire方法用于保证当前线程的前驱节点的waitStatus属性值为SIGNAL,从而保证了自己挂起后,前驱节点会负责在合适的时候唤醒自己。
  • parkAndCheckInterrupt方法用于挂起当前线程,并检查中断状态。
  • 如果最终成功获取了锁,线程会从lock()方法返回,继续往下执行;否则,线程会阻塞等待。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值