AQS互斥模式源码解析

AbstractQueuedSynchronizer

我们知道,Java中很多重要的并发组件都是基于AQS进行设计的,AQS本身是一个类,但是不如说他是一个框架,该框架为众多并发组件提供了底层基础。本篇文章致力于分析AQS的源码,以了解AQS的执行机制。在讨论AQS的具体逻辑之前,首先我们讨论AQS的父类AbstractOwnableSynchronizer。

AbstractOwnableSynchronizer

AQS,全称就是AbstractQueuedSynchronizer,在java.util.concurrent.locks包中,下面是它的类继承结构图:

在这里插入图片描述

该类的类继承结构极其简单,AbstractQueuedSynchronizer仅仅继承了AbstractOwnableSynchronizer。我们可以先查看AbstractOwnableSynchronizer的功能以及源码。该类代码如下:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    private static final long serialVersionUID = 3737899427754241961L;
    
    protected AbstractOwnableSynchronizer() { }

    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

可以看到AbstractOwnableSynchronizer是一个只有一个属性的抽象类,其属性只有exclusiveOwnerThread

private transient Thread exclusiveOwnerThread;

并且也只有两个方法,即exclusiveOwnerThread属性的getter和setter方法。而且我们甚至不能对其中的方法进行任何覆盖操作,那它用来做什么呢?

查看AbstractOwnableSynchronizer的注释,我们可以发现,该类是一个同步器,用于保存独占模式下,持有资源的线程。

/**
 * The current owner of exclusive mode synchronization.
 */
private transient Thread exclusiveOwnerThread;

我们查看exclusiveOwnerThread的属性名,也可以发现,该属性名字的含义是专有的拥有者线程。对于一个抽象的同步器来说,为什么要继承如此一个超类呢?

我们考察一个分布式锁的逻辑。A资源不允许两个系统B、C同时修改,现在我们要加一个分布式锁来限制这件事。怎么处理呢?在B系统准备修改A资源之前,先检查一下Redis,是否有一个叫做A_Lock的key存在,如果有,则等到这个key被删除之后再进行下一步,如果没有,先在Redis中添加一个key名为A_Lock的记录,然后再操作A资源,操作结束后,删除key为A_Lock的记录。C系统同理。

上面的情况非常常见的,而这个key为A_Lock的记录就是我们这里AbstractQueuedSynchronizerexclusiveOwnerThread属性。我们可以查看一下setExclusiveOwnerThread(Thread thread)方法的调用方,可以发现都是如下两个方法:

  1. tryAcquire(int unused)方法:都是setExclusiveOwnerThread(current);
  2. tryRelease(int unused)方法:都是setExclusiveOwnerThread(null);

这两个方法通过方法名我们就知道是用来加锁、释放锁的,可以看到,加锁时将线程放入到exclusiveOwnerThread中,解锁时,则放null进去。

至此,我们就了解了AbstractOwnableSynchronizer的作用。下面让我们专心分析AbstractQueuedSynchronizer

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer基于AbstractOwnableSynchronizer完成了自己的工作,那么AbstractQueuedSynchronizer到底完成了什么工作呢?查看注释我们可以看到:

/**
 * This class is designed to
 * be a useful basis for most kinds of synchronizers that rely on a
 * single atomic {@code int} value to represent state.
 * /

上面注释说到,该类是大多数依赖单个原子性int值表示同步状态的同步器的基础。那么该类到底是怎么实现该功能的呢?注释的第一句话就有说明:

/**
 * Provides a framework for implementing blocking locks and
 *  related synchronizers (semaphores, events, etc) that rely on
 *  first-in-first-out (FIFO) wait queues.
 * /

可以看到,该类实现上述功能依赖了FIFO的阻塞队列。但是,Java中的阻塞队列不应该是基于各种锁,同步器么?这样不就出现了循环依赖么?那么AbstractQueuedSynchronizer是使用的怎样的阻塞队列呢?我们考察该类的属性,可以看到该类与队列相关的属性很少,只有两个:

private transient volatile Node head;
private transient volatile Node tail;

显而易见,这就是阻塞队列的头结点和尾节点,链表本身就用于存储数据,让我们考察一下该类的Node实现:

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    static final int CANCELLED =  1;
    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;
}

可以看到Node节点有4个属性,分别是:

  1. waitStatus:表示当前节点包含线程的状态。该状态有5种:
    1. CANCELLED:这意味着该线程已经因为超时或者中断而取消了,这是一个终结态。
    2. SIGNAL:表示当前线程的后继线程被阻塞或者即将被阻塞,当前线程释放锁或者取消后需要唤醒后继线程,注意这个状态一般都是后继线程来设置前驱节点的。
    3. CONDITION:说明当前线程在CONDITION队列
    4. PROPAGATE:用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制。在一个节点成为头节点之前,是不会跃迁为此状态的
    5. 0:表示无状态
  2. prev:前驱节点
  3. next:后继结点
  4. thread:该节点表示的线程
  5. nextWaiter:SHARED or EXCLUSIVE,该属性描述锁的类型,而且需要注意,用来描述锁类型的是一个Node节点,而不是一个int值。

Node类的注释详细解释了该队列的运行逻辑,该队列名为CLH队列,是以三个人的名字命名的。但是由于注释实在是太长了。这里笔者对其简化,这里介绍一下。

CLH队列+自旋锁是构造一个同步器的基础方案,AQS就是采用了这种方法,对于CLH队列,其最重要的特点就是当前节点根据当前节点的前驱节点判断前一个线程是否释放了锁。这一点十分重要。除此之外该队列与普通的双向队列区别不大。接下来我们会根据AQS的源码分析CLH队列和CAS到底是怎么完成同步器的功能的。

AQS中加锁、解锁的方法有很多,例如:

  1. acquire(int arg):获取互斥锁
  2. acquireInterruptibly(int arg):获取互斥锁,外界可以对加锁过程进行中断
  3. acquireShared(int arg):获取共享锁
  4. acquireSharedInterruptibly(int arg):获取共享锁,外界可以对加锁过程进行中断
  5. release(int arg):释放互斥锁
  6. releaseShared(int arg):释放共享锁

让我们首先分析acquire(int arg)release(int arg)方法,即互斥锁的加锁与解锁流程,因为这两个方法相对最简单,也最能让人理解CLH队列的功能。

1. acquire(int arg)

acquire(int arg)方法用于获取互斥锁,让我们考察该方法源码:

public final void acquire(int arg) {
    // 尝试获取锁
    // 如果没有获取到,则创建一个节点放入队列中
    // 等待获取到锁,如果获取过程中,出现特殊情况,
    // 则中断当前线程,避免阻塞
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAquire(int)方法负责尝试获取锁,但是AQS实际上并没有对这个方法的具体实现,而子类可以覆盖该方法,也正因为如此,才有了ReentrantLock,这一系列的同步器。

这里我们只需要知道在使用AQS获取互斥锁时,都是先调用tryAquire(int)尝试获取锁的,如果获取不到,就会将当前线程的信息存储到我们之前说的CLH队列中,至于存储的过程,让我们考察一下addWaiter(Node mode)方法,源码如下:

   private Node addWaiter(Node mode) {
        // 创建一个Node,并通过传入参数设计其加锁模式
        // 监听的线程就是当前线程
        Node node = new Node(Thread.currentThread(), mode);
        // 获取队尾节点
        // 如果队列不为空,那么将当前节点与队尾节点连接
        // 然后执行CAS操作,将当前节点添加到pred后面自旋锁
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果队列是空,那么就创建一个空节点作为队列头,然后将新节点插入
        enq(node);
        return node;
    }

可以看到,该方法传入了一个Node节点,注意这个Node节点并不是我们要创建的节点,它只是一个标识,标识要创建的节点标识互斥还是同步真正的线程数据其实根本不需要通过参数传输,因为现在本身就是在这个线程中

接下来让我们查看一下enq(final Node)中的细节,该方法用于处理队列是空,创建一个空节点作为队列头,然后将新节点插入的情况,源码如下:

	private Node enq(final Node node) {
        // 自旋+CAS 将Node添加到队列中
        for (;;) {
            Node t = tail;
            // 如果队列为空
            // 这时创建一个New Node作为head和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;
                }
            }
        }
    }

这里有一个细节需要注意,如果队列为空,会创建一个空的Node最为队列头,然后再将要添加的Node加入到队列尾部。其中新构建的队列头节点,waitStatus=0。这里就开始暴露了CLH队列的特点,通过前驱节点判断前一个线程是否释放锁。接下来我们继续向下看:

将线程信息放入到队列中后,就要开始获取锁了,acquireQueued方法用于持续的获取锁。在使用锁的时候,我们会看到,如果一个线程未获取到锁,会一直阻塞,直到获取到锁为止,该方法就是造成上述现象的主要原因,源码如下:

	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋+获取锁
            for (;;) {
                final Node p = node.predecessor();
                // 获取node的前一个节点,查看是否是头结点
                // 如果是头结点再进行获取锁
                // 如果获取到了,就将队列头设置为它
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 在没有成功获取锁的情况下判断是否应该中断获取锁
                // 判断逻辑在`shouldParkAfterFailedAcquire`方法中
                // 挂起当前线程,避免占用资源。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果没有成功获取锁,那么要唤醒线程,并进行一系列的处理
            if (failed)
                cancelAcquire(node);
        }
    }

这里我们看到,acquireQueued(final Node node, int arg)方法会通过自旋的方式不断检测当前节点的前驱节点是否是头结点,如果是,则尝试获取锁。如果获取到了锁,就将其设置为头结点。这就意味着,能够有机会竞争锁的永远是CLH队列的头结点的下一个节点,而头结点代表的线程是真正拥有锁的线程。而且有一个小细节,如果某个线程没有获取到锁,难道要一直不断进行自旋,然后CAS竞争锁么?那如果有20个线程在等待锁,资源岂不是很大的浪费。我们考察shouldParkAfterFailedAcquire(p, node)方法,通过方法名我们知道,如果竞争锁失败,那么线程就会被挂起。考察源码如下:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 如果当前节点的前驱节点的状态是`SIGNAL`,
            // 这意味着前驱节点线程占用的锁释放之后,就会分配给当前节点的线程
            // 因此挂起当前线程就可以了,直接返回true
            return true;
        if (ws > 0) {
            // 如果前驱节点的状态是`CANCEL`,这意味着前驱节点的任务已经被取消了,
            // 这意味着要使用第一个非`CANCEL`状态的节点与当前节点进行对比
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 修改指针
            pred.next = node;
        } else {
            // 到达这里证明前驱节点的状态是0或者PROPAGATE
            // 那么这时候需要前驱节点线程释放锁之后通知当前节点线程
            // 因此需要修改前驱节点线程状态为Node.SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

我们知道,AQS中Node的状态有5个,分别表示:

  1. CANCELLED:这意味着该线程已经因为超时或者中断而取消了,这是一个终结态。
  2. SIGNAL:表示当前线程的后继线程被阻塞或者即将被阻塞,当前线程释放锁或者取消后需要唤醒后继线程,注意这个状态一般都是后继线程来设置前驱节点的。
  3. CONDITION:说明当前线程在CONDITION队列
  4. PROPAGATE:用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制。在一个节点成为头节点之前,是不会跃迁为此状态的
  5. 0:表示无状态

这里我们不考虑CONDITIONPROPAGATE,这两个状态与互斥模式关系不大。主要考虑剩下三个状态。我们已经知道,CLH队列规则是通过前驱节点状态判断前一个线程是否释放锁,接下来考虑三种状态:

  1. SIGNAL:如果前一个线程的状态是SINGAL,这说明,前一个线程释放锁会通知后一个线程,也就是当前线程获取锁。那样当前线程直接挂起等着就好了。
  2. CANCELLED:如果前一个线程被取消了,不获取锁了,那么就应该与当前节点的前一个非CANCELLED的节点比较再进行决定,因此这里先不进行挂起操作,更新一下节点,然后再去尝试获取一次锁,如果获取不到,再该挂起挂起。
  3. 0:如果前驱节点的状态为0,这个就是我们之前说过的,队列中的第一个节点嘛,我们都知道这个节点就是个标志性的节点,直接将他设置为SIGNAL,然后再次尝试获取锁,如果获取到了,那就最好,获取不到,在前一线程唤醒后也会通知当前线程。

如果shouldParkAfterFailedAcquire(Node,Node)返回true,那么意味着当前Node已经加入到阻塞队列中了,因此需要挂起当前线程,直到当前节点的前驱节点释放锁为止。parkAndCheckInterrupt()方法就负责挂起当前线程:

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

此时线程就被挂起了,知道有其他线程唤醒该线程为止。上面就介绍完了获取互斥锁成功的全过程,下面给出图示,方便理解AQS在多个线程获取锁时CLH队列是如何变化的。

我们假设有ABC三个线程同时获取锁,我们假设,他们调用acquire(int)的时序是A->B->C,并且规定A线程会持续持有锁,直到C线程获取完毕锁之后才会释放锁。为了达到这种状态,使用了如下代码:

public class TestThread extends Thread{

    Lock lock;
    public TestThread(Lock lock) {
        this.lock = lock;
    }
    @Override
    public void run() {
        lock.lock();
        System.out.println(this.getName()+" lock");
    }
}

public class Main {

    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock(false);
        new TestThread(reentrantLock).start();
        new TestThread(reentrantLock).start();
        new TestThread(reentrantLock).start();
    }
}

虽然实现方式比较暴力,但是这是我能想到的,最简单而且没有其他并发包组件影响的实现方式了,就先使用该方式实现吧。让我们考虑加锁流程:

  1. A线程由于是第一个获取锁的,因此,直接获取到了锁。并没有往AQS的CLH队列中添加节点。所以此时,AQS队列中是空的。

  2. B线程是第二个获取锁的,此时,由于A线程在持有锁,所以tryAquire(int)方法执行失败。此时往CLH队列中添加节点,将B线程信息放入到CLH队列中。由于当前队列是空的,所以需要添加两个节点,队列头和保存B线程数据的节点。在执行完addWaiter(Node)方法后,CLH队列状态如下:

    在这里插入图片描述

  3. 将B线程的节点放入到CLH队列中后,需要进行CAS+自旋获取锁,也就是执行acquireQueued(Node,int)方法。acquireQueued(Node,int)方法会尝试获取一次锁,此时由于A线程还未释放锁,因此,获取失败,此时,CLH队列仍然是上面的状态。由于获取失败,会尝试判断B线程是否要挂起,则调用shouldParkAfterFailedAcquire(Node,Node)方法。此时,由于保存B信息的前一个节点的waitStatus=0,因此,会将其从0改为SIGNAL。然后返回false,再次尝试获取锁。此时状态如下:

    在这里插入图片描述

  4. 经过第三步后,B线程再次尝试获取锁,此时,由于A线程仍然没有释放锁,因此,再次调用shouldParkAfterFailedAcquire(Node,Node)方法,返回true,表示B线程可以被挂起,所以挂起B线程。

  5. 此时C线程开始获取锁,同样由于A线程持有锁,所以,调用`addWaiter(Node)方法,向CLH中添加一个节点。调用之后,CLH队列状态如下:

    在这里插入图片描述

  6. 将C线程信息添加到CLH队列中后,开始调用acquireQueued(Node,int)尝试获取锁,这次同样获取不到锁,因为A线程仍然持有锁,因此,会调用shouldParkAfterFailedAcquire(Node,Node)方法判断是否要挂起线程,因为B线程节点waitStatus=0,所以shouldParkAfterFailedAcquire(Node,Node)方法返回false,并将B线程节点的waitStatus设置为SIGNAL,CLH队列状态如下:

    在这里插入图片描述

  7. 最后C线程再次获取一次锁,发现仍然获取不到,这时候再判断挂起,发现应该挂起,此时就挂起C线程。然后结束了。

挂起的线程会在持有锁的线程释放锁之后被唤醒,这部分会在release(int)方法中体现。当挂起的线程被唤醒之后,由于循环的原因会再次尝试获取锁。

这里给出正常加锁时的流程图:

在这里插入图片描述

前面讨论和演示的是正常加锁的逻辑,事实上,有一部分的线程的加锁过程会被强制中断,或者由于某些异常而无法正常获取到锁,这时AQS为了让其他的线程正常获取锁,就会对这类无法获取到锁的节点进行处理,处理工作由cancelAcquire(Node node)方法完成。我们首先查看何时会进行这个处理获取锁失败的节点的操作,再次查看acquireQueue(Node,int)方法源码:

```java
	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋+获取锁
            for (;;) {
                final Node p = node.predecessor();
                // 获取node的前一个节点,查看是否是头结点
                // 如果是头结点再进行获取锁
                // 如果获取到了,就将队列头设置为它
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 在没有成功获取锁的情况下判断是否应该中断获取锁
                // 判断逻辑在`shouldParkAfterFailedAcquire`方法中
                // 挂起当前线程,避免占用资源。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果没有成功获取锁,那么要唤醒线程,并进行一系列的处理
            if (failed)
                cancelAcquire(node);
        }
    }

我们注意,这里只有在false为true,并且循环被停止的情况下,会执行cancelAcquire(node);能达到这个目的的只有抛出异常。那么上述代码哪里会抛出异常呢?

  1. final Node p = node.predecessor();会抛出空指针异常:

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

    如果当前节点的前驱节点是null,那么会抛出空指针异常。不过一般不会出现这种情况,因为AQS的设计十分严谨。

  2. tryAcquire(arg)该方法是使用AQS的客户端实现的,可能会抛出RuntimeException

  3. parkAndCheckInterrupt()该方法负责将线程挂起,但是有些情况线程会抛出InterruptedException

当然上面只列出了笔者能想到的情况,可能还有别的部分会导致循环中断。但是无论出现哪种情况,都会由cancelAcquire(node);方法进行处理,下面我们查看cancelAcquire(node);方法源码:

    private void cancelAcquire(Node node) {
        if (node == null)
            return;
        // 清除Node数据
        node.thread = null;
       	// 如果该Node的前驱节点都是被取消的任务
    	// 那么将该Node添加到最后一个可用任务之后
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        // 获取当前节点的前驱节点的下一个节点,用于进行后面的CAS操作
        Node predNext = pred.next;

        // 将获取锁不成功的节点设置为CANCELLED,避免影响其他节点工作
        node.waitStatus = Node.CANCELLED;

        // 如果当前节点是尾部节点,那么就使用CAS将尾部节点设置为前一个节点,目的是为了清除当前节点
        // 情况1
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // 由于有些情况下,队列中有的节点的状态是SIGNAL,因此这类节点被删除时,需要将其前一个可用节点的状态设置为SIGNAL
            // 该部分代码就用于处理该问题
            // 情况2
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
            // 情况3
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

首先介绍AQS处理获取锁失败的节点的统一步骤,即如下代码:

		if (node == null)
            return;
        // 清除Node数据
        node.thread = null;
       	// 如果该Node的前驱节点都是被取消的任务
    	// 那么将该Node添加到最后一个可用任务之后
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        // 获取当前节点的前驱节点的下一个节点,用于进行后面的CAS操作
        Node predNext = pred.next;

        // 将获取锁不成功的节点设置为CANCELLED,避免影响其他节点工作
        node.waitStatus = Node.CANCELLED;

当AQS认定该节点是获取锁失败的节点后,会统一先将节点的thread设置为null,然后通过链表操作,将获取锁失败的节点之前所有的CANCELLED的节点全部删除。最后将节点状态设置为CANCELLED。这里其实带来了一些问题,这个问题我们到下一节再提。

处理获取锁失败的节点(为了方便描述,接下来统一将这类节点都称为失败节点)时分为三种情况:

  1. 失败节点是CLH队尾节点

    处理这部分逻辑的代码如下:

    if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
    }
    

    处理逻辑就是将错误节点的前一个节点设置为队尾,然后将前一节点的下一节点设置为null。注意这里没有使用CAS+自旋的方式。因为事实上队尾节点的下一节点是否为null并不重要,因为addWaiter(Node)创建新的队尾节点时,并没有对tail.next进行验证,所以这里就算没有设置成功也没有什么关系。处理过程如下图所示:

    在这里插入图片描述

  2. 失败节点的前驱节点不是队列头节点、其状态不是CANCELLED,而且其代表的线程不为null。
    处理这部分逻辑的代码如下:

    		int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            }
    

    由于此种情况下,失败节点应该在CLH的中间位置,而且其前驱节点在我们讨论的互斥锁的情况下理论上应该是SIGNAL状态,除非其前驱节点获取锁也失败了。这时如果后继节点的waitStatus<=0,就将前驱节点的下一个节点设置为后继结点。处理过程如下:

    在这里插入图片描述
    这里可以看到一个特别的情况,next的前驱节点仍然是node,我们都知道在调用acquireQueue(Node,int)尝试获取锁时会判断当前节点的前驱节点是否为head。如果出现这种情况,那岂不是永远都不会让next的前驱节点变为head?实际上并不是这样的。如果节点获取不到锁会进入shouldParkAfterFailedAcquire(Node pred, Node node)方法,该方法并不仅仅只是判断该线程是否应该park,而且还将当前节点之前所有的CANCELLED状态的节点消除了。所以就不会有这个问题了。当然如果next节点也获取锁失败了,那也可能是cancelAcquire(Node node)方法处理这个问题。

  3. 失败节点的前驱节点是头结点,或者失败节点的前驱节点状态是CANCELLED或者最后失败节点的前驱节点代表的thread=null。
    需要注意,上面的设置、判断等一系列操作,都是通过一次CAS进行操作或者判断的,因此可能在处理到一半就中断了,例如第二步,可能之前的很久时间失败节点的前驱节点是SIGNAL状态的,但是在判断的那一刻突然变成了-1。那么此时都要交给第三种情况处理。这也就意味着第三种情况的处理方案应该是最通用的。让我们来查看该情况的处理方案:

    unparkSuccessor(node);
    

    该方法负责唤醒node节点代表的线程的下一个线程,那么为什么说它是通用的呢?考虑unparkSuccessor(Node node)方法源码:

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 如果节点状态小于0,那么将节点状态设置为0
        // 这样会使后继节点再次获取一次锁
        // 因为如果前驱节点waitStatus为0,会在获取锁失败后将前驱节点的waitStatus设置为SIGNAL后再次进行获取锁
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        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);
    }
    

    可以看到该方法总共做了两件事:

    1. 将当前节点waitStatus设置为0,使得后继节点能够再次获取一次锁
    2. 查找下一个要唤醒的线程,然后将其进行唤醒,查找逻辑有两种:
      1. 查看当前节点的后继节点是否是null,并且waitStatus <=0。 如果是的话,就激活该节点。
      2. 否则从队列尾部向前查找,找到最后一个waitStatus<=0的节点,唤醒它。

至此aquire(int)方法逻辑分析完毕,我们也了解了AQS到底如何利用CLH队列的。

2. release(int arg)

release(int arg)方法是aquire(int)的逆过程,主要用于释放独占锁,相比于aquire(int)来说,release(int arg)的逻辑相对简单的多:

public final boolean release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
        // 如果释放成功则尝试唤醒下一个等待锁的线程
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

可以看到,该方法仅仅是调用一次tryRelease(int)方法,然后,根据头结点信息,修改头结点业务信息,判断是否要触发后面线程获取锁,即调用unparkSuccessor(Node)方法,该方法的源码上面已经介绍过,这里不再赘述。

最后同样给出release(int)方法的流程图,方便理解。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值