AQS源码详解

3,454 阅读22分钟

什么是 AQS?

AQS(AbstractQueuedSynchronizer)核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点 Node 来实现锁的分配。

CLH 队列如图:

队列同步器 AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 类型的成员变量 state 表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法 getState()setState(int newState)compareAndSetState(int expect, int update) 来进行操作,因为它们能够保证状态的改变是安全的。

子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLockReentrantReadWriteLockCountDownLatch等)。

同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

重写同步器指定的方法时,需要使用同步器提供的如下 3 个方法来访问或修改同步状态:

  • getState():获取当前同步状态
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect,int update):使用 CAS 设置当前状态,该方法能够保证状态设置的原子性

同步器可重写的方法如下表所示。

方法名称 描述
protected boolean tryAcquire(int arg) 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行 CAS 设置同步状态
protected boolean tryRelease(int arg) 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于 0 的值,表示获取成功,反之,获取失败
protected boolean tryReleaseShared(int arg) 共享式释放同步状态
protected boolean isHeldExclusively() 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占

实现自定义同步组件时,会调用同步器提供的模板方法,如下图所示。

image-20200606212156471

同步器提供的模板方法分为 3 类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。

Mutex 独占锁的实现

只有掌握了同步器的工作原理才能更加深入地理解并发包中其他的并发组件,所以下面通过一个独占锁的示例来深入了解一下同步器的工作原理。

顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后续线程才能够继续获取锁。

Mutex 的代码实现如下:

public class Mutex implements Lock {
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -4387327721959839431L;

        // 是否处于占用状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 当状态为0的时候获取锁
        @Override
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁,将状态设置为0
        @Override
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 返回一个Condition,每个condition都包含了一个condition队列
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    // 仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

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

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

上述示例中,独占锁 Mutex 是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex 中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在 tryAcquire(int acquires) 方法中,如果经过 CAS 设置成功(同步状态设置为 1),则代表获取了同步状态,而在 tryRelease(int releases) 方法中只是将同步状态重置为 0。用户使用 Mutex 时并不会直接和内部同步器的实现打交道,而是调用 Mutex 提供的方法,在 Mutex 的实现中,以获取锁的 lock() 方法为例,只需要在方法实现中调用同步器的模板方法 acquire(int args) 即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。

AQS 原理

接下来将从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放、超时获取同步状态等同步器的核心数据结构与模板方法。

同步队列

同步器依赖内部的同步队列(一个 FIFO 双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个 Node 节点并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把头节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列中的 Node 节点用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下所示。

Node 节点有如下几种状态:

// 因为超时或者中断,节点会被设置成取消状态,被取消的节点不会参与到竞争中,保持取消状态不会改变
static final int CANCELLED =  1;

// 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消了,将会通知后继节点,使后继节点的线程得以运行
static final int SIGNAL    = -1;

// 表示节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中
static final int CONDITION = -2;

// 表示下一次共享式同步状态获取将会无条件被传播下去
static final int PROPAGATE = -3;

Node 中还定义了不同的节点:

// 后继节点
volatile Node next;

// 前驱结点,当节点加入同步队列时被设置(尾部添加)
volatile Node prev;

// 获取同步状态的线程
volatile Thread thread;

// 等待队列中的后继节点。如果当前节点是共享的,那么该字段是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共有一个字段
Node nextWaiter;

节点是构成同步队列的基础,同步器拥有头节点 head 和尾节点 tail,没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如下图所示。

在上图中,同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态,其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于 CAS 设置尾节点的方法:compareAndSetTail(Node expect, Node update),它需要传递当前线程“认为"的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

同步队列遵循 FIFO,头节点是获取同步状态成功的节点,头节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为头节点,该过程如下图所示。

在上图中,设置头节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用 CAS 来保证,它只需要将头节点设置成为原头节点的后继节点并断开原头节点的next引用即可。

独占式同步状态的获取与释放

通过调用同步器的 acquire(int arg) 方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,acquire 方法代码如下:

public final void acquire(int arg) {
    // 尝试获取锁
    if (!tryAcquire(arg) &&
        // 获取不到,则进入等待队列,返回是否中断
        // acquireQueued返回true表示中断
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果返回中断,则调用当前线程的interrupt()方法
        selfInterrupt();
}

上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用自定义同步器实现的 tryAcquire(int arg) 方法(由子类实现该方法),该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过 addWaiter(Node node) 方法将该节点加入到同步队列的尾部,最后调用 acquireQueued(Node node, int arg) 方法,使得该节点以自旋方式获取同步状态。如果获取不到则调用 LockSupport.park(this) 阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队阻塞线程被中断来实现。

下面分析一下相关工作,首先是节点的构造以及加入同步队列。

private Node addWaiter(Node mode) {
    // 将当前线程封装成Node,并且mode为独占锁
    Node node = new Node(Thread.currentThread(), mode);
    // tail是AQS的中表示同步队列的队尾,刚开始为null,所以进行enq(node)方法
    Node pred = tail;
    if (pred != null) {
        // 将当前节点node的前驱结点设置为尾结点
        node.prev = pred;
        // CAS设置尾结点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 第一次添加,tail=null,将node添加到同步队列中
    enq(node);
    return node;
}

private Node enq(final Node node) {
    // lockfree:循环+CAS
    for (;;) {
        Node t = tail;
        // 如果是第一次添加到队列,那么tail=null
        if (t == null) { 
            // CAS设置头节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } 
        // 否则添加逻辑和addWaiter中相似
        // 既然相似,这里为什么需要再次判断不为null呢?
        // 假设有两个线程同时进入enq方法,先进来的线程判断 t==null 表示队列是首次使用,需要先初始化,
        // 那么第二个线程判断 if(t==null) 不通过,如果没有else逻辑,那么第二个线程就无法执行,
        // 因此需要和addWaiter相似的逻辑,保证多线程情况下也正常。
        else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

上述代码通过使用 compareAndSetTail(Node expect, Node update) 方法来确保节点能够被线程安全添加。

enq(final Node node) 方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过 CAS 将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node) 方法将并发添加节点的请求通过 CAS 变得“串行化”了。

节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),同步器的 acquireQueued 方法如下所示。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 其前驱是头结点,并且再次调用tryAcquire成功获取锁
            if (p == head && tryAcquire(arg)) {
                // 将自己设为头结点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                // 成功获取锁,返回
                return interrupted;
            }
            // 没有得到锁时
            // shouldParkAfterFailedAcquire方法:返回是否需要阻塞当前线程
            // parkAndCheckInterrupt方法:阻塞当前线程,当线程再次唤醒时,返回是否被中断
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 修改中断标志位
                interrupted = true;
        }
    } finally {
        if (failed)
            // 获取锁失败,则将此线程对应的node的waitStatus改为CANCEL
            cancelAcquire(node);
    }
}

/*
 * 获取锁失败时,检查并更新node的waitStatus。
 * 如果线程需要阻塞,返回true。
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;

    // 前驱节点的waitStatus是SIGNAL
    if (ws == Node.SIGNAL)
        /* 
         * SIGNAL状态的节点,释放锁后,会唤醒其后继节点。
         * 因此,此线程可以安全的阻塞(前驱节点释放锁时,会唤醒此线程)。
         */
        return true;

    // 前驱节点对应的线程被取消,CANCELLED = 1
    if (ws > 0) {
        // 需要将取消状态的节点从队列中移除直到找到一个状态不是取消的节点为止
        do {        
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 除了以上情况,通过CAS将前驱节点的状态设置成SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

// 当shouldParkAfterFailedAcquire方法返回true,则调用parkAndCheckInterrupt方法阻塞当前线程
private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程
    LockSupport.park(this);
    // 判断当前线程是否被中断,如果中断了,则返回true
    // 由于Thread.interrupted()方法会清除中断标志位,所以后续需要修改还原成true
    return Thread.interrupted();
}

acquireQueued(final Node node, int arg) 方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,原因有以下两点:

  • 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
  • 维护同步队列的 FIFO 原则

该方法中,节点自旋获取同步状态的行为如下图所示。

在上图中,由于非头节点线程的前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合 FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。

独占式同步状态获取流程:

对于每个得不到锁的线程,都会把自己封装成 Node节点,加入队尾,然后判断当前节点的前驱节点是否是头节点,如果是则尝试获取锁,尝试获取成功则把自己置为头节点;如果获取失败,则继续检查前驱节点的状态,如果状态为 SIGNAL(-1),则当前节点进入阻塞状态(前驱节点可以唤醒自己,降低自旋开销);如果是除了 CANCELLED(1) 以外的其他状态,则修改为SIGNAL(-1)。当又新增一个节点时,首先自旋判断当前节点的前驱节点是否为头结点,如果不是则直接判断状态是否是SIGNAL(-1),是的话就阻塞,如果是除了 CANCELLED(1) 以外的其他状态,则修改为SIGNAL(-1)

因此整个过程就是时而自旋,时而阻塞,直到获取锁或者被取消。

相关图解可以参考https://www.jianshu.com/p/b6efbdbdc6fa这篇文章。

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的 release(int arg) 方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。同步器的 release 方法如下所示。

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

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 如果当前节点的状态小于0,那么用CAS设置成0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    
    // 获取当前节点的后继节点
    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)
        // 唤醒后继节点
        LockSupport.unpark(s.thread);
}

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node) 方法使用 LockSupport 来唤醒处于等待状态的线程。

总结

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg) 方法释放同步状态,然后唤醒头节点的后继节点。

共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,如下图所示。

Snipaste_2019-07-06_14-23-18.png

在上图中,左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞。右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。

通过调用同步器的 acquireShared(int arg) 方法可以共享式地获取同步状态,该方法代码如下所示。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);

    // propagate如果>0,说明我这次获取共享锁成功后,还有剩余共享锁可以获取
    // 如果=0,说明我这次获取共享锁成功后,没有共享锁可以获取

    /*
         如果propagate > 0,说明还有剩余共享锁可以获取,那么说明后继节点需要被唤醒。
         如果propagate = 0,说明没有剩余共享锁可以获取了,按理说不需要唤醒后继的。
         但是如果h.waitStatus < 0,这说明之前head的waitStatus < 0,不过在这之前waitStatus都被置为了0,
         只有一种可能会让waitStatus < 0
             由于doReleaseShared里的compareAndSetWaitStatus(h, 0, Node.PROPAGATE)的操作,
             有另一个线程在调用doReleaseShared才能造成,而这很可能是因为在中间状态时,又有人释放了共享锁
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 如果当前节点的后继节点是共享类型或者当前节点没有后继节点,则进行唤醒
        // 这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
        if (s == null || s.isShared())
            // 唤醒获取到共享锁节点的后继节点
            doReleaseShared();
    }
}

acquireShared(int arg) 方法中,同步器调用 tryAcquireShared(int arg) 方法尝试获取同步状态, tryAcquireShared(int arg) 方法返回值为 int 类型,当返回值大于等于 0 时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg) 方法返回值大于等于 0。

可以看到,在 doAcquireShared(int arg) 方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于 0,表示该次获取同步状态成功并从自旋过程中退出。

与独占式一样,共享式获取也需要释放同步状态,通过调用 releaseShared(int arg) 方法可以释放同步状态,该方法代码如下所示。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 如果头结点不为空 && 头结点不等于尾结点,说明存在有效的node节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果头结点的状态为SIGNAL(-1),说明存在需要唤醒的后继节点
            if (ws == Node.SIGNAL) {
                // 将头结点状态更新为0(初始值状态),此时头结点已经没用了
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果头结点为初始值状态0,则设置为PROPAGATE(-3),确保在释放同步状态时能通知后继节点
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        
        //如果头结点没有发生变化,表示设置完成,退出循环
        //如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
        if (h == head)                   // loop if head changed
            break;
    }
}

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于 tryReleaseShared(int arg) 方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和 CAS 来保证的,因为释放同步状态的操作会同时来自多个线程。

注意:setHeadAndPropagate 方法表示等待队列中的线程成功获取到共享锁,这时候它需要唤醒它后面的共享节点,但是当通过 releaseShared 方法去释放一个共享锁的时候,接下来等待独占锁跟共享锁的线程都可以被唤醒进行尝试获取。

总结

跟独占锁相比,共享锁的主要特征在于当一个在等待队列中的共享节点成功获取到锁以后(它获取到的是共享锁),既然是共享,那它必须要依次唤醒后面所有可以跟它一起共享当前锁资源的节点,毫无疑问,这些节点必须也是在等待共享锁(这是大前提,如果等待的是独占锁,那前面已经有一个共享节点获取锁了,它肯定是获取不到的)。当共享锁被释放的时候,可以用读写锁为例进行思考,当一个读锁被释放,此时不论是读锁还是写锁都是可以竞争资源的。

总结

独占锁和共享锁在源码层面的区别在于:

独占锁只有当前锁显式调用 release 方法释放锁后,才能唤醒 CLH 等待队列的后继节点。

共享锁有一个 setHeadAndPropagate 方法,在这个方法中可以唤醒等待队列的后继共享节点,而不一定必须显式调用 releaseShared 方法。