AbstractQueuedSynchronizer 源码分析

概述

今天聊下并发编程中很重要的一个类AbstractQueuedSynchronizer,简称AQS,为什么说他重要?并发库作者Doug Lea设计之初就是期望它能够成为实现大部分同步需求的基础。因此看懂了它,将很容易学习(ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等),而且使用它可以很容易搭建自定义的同步器。

AQS使用的是典型的模版方法模式,子类自定义同步器时只需要实现少数的几个方法就行,屏蔽了大量的细节,例如获取同步状态、FIFO同步队列。基于AQS不仅能极大的降低开发工作量,而且降低了并发竞争位置时出错的概率。

AQS提供独占锁和共享锁两种获取锁的方式。
独占式exclusive:保证一次只有一个线程可以获取到锁。
共享式shared:允许多个线程同时获取到锁。

public abstract class AbstractQueuedSynchronizer implements java.io.Serializable {
    protected AbstractQueuedSynchronizer() {}

    //同步队列中的节点
    static final class Node {}

    //队列的头节点
    private transient volatile Node head;
    //队列的尾节点
    private transient volatile Node tail;
    //同步状态: state=0时表示无线程锁定, state>0时表示有线程锁定中
    private volatile int state;
    ...
}

可以看到AQS中维护了一个队列和一个同步状态state。
队列: 是一个CHL队列(FIFO),用来存储等待获取锁的线程。
state: 是同步的状态,等于0表示未锁定,大于0表示已有线程锁定。
队列结构图如下:

image

接下来我们看下节点Node类的内部:

static final class Node {
    //等待状态:有SIGNAL,CANCELLED,CONDITION,PROPAGATE,0
    volatile int waitStatus;
    //当前节点的前节点
    volatile Node prev;
    //当前节点的后节点
    volatile Node next;
    //节点的线程: 毕竟竞争的主体是线程
    volatile Thread thread;
    //存储condition队列中的后继节点。
    Node nextWaiter;
    ...
}

通过prev、next可以看到节点是双向引用的,我们重点关心等待状态waitState,waitState取值的时机一定要清楚。

  • 0: 新节点入队是的默认值。
  • CANCELLED (1): 表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL (-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
    注意: 负值表示结点处于有效等待状态,而正值表示结点已被取消。源码中很多地方用>0、<0来判断结点的状态是否正常。

下面我们一起看一下AQS最重要的几个方法:acquire(独占式获取锁)、release(独占式释放锁)、acquireShared(共享式获取锁)、releaseShared(共享式释放锁)

acquire 独占式获取锁

public final void acquire(int arg) {
    //尝试获取锁,获取成功则返回,否则加入等待队列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. tryAcquire尝试获得锁,因为是短路与(&&),tryAcquire成功,!tryAcquire()为false,则跳出if,不在进行后续操作。
  2. 如果获取失败则进入addWaiter方法,构造同步节点(独占式Node.EXCLUSIVE),将该节点添加到同步队列尾部,并返回此节点,进入acquireQueued方法。
  3. acquireQueued中如果前继节点是头节点,再次尝试换取锁,失败则将正常前继节点的waitState设置为SIGNAL,然后阻塞自己,那么该线程被唤醒就只可能是前继节点发起唤醒,或者线程被中断。该方法有一点歧义,返回值其实是中断状态,很容易让人以为是是否请求队列成功,这个后面会详细说明。
  4. 所以当if条件为真,就是没有请求成功,且线程被中断了,则再次执行中断方法selfInterrupt。

tryAcquire失败后进入addWaiter方法

private Node addWaiter(Node mode) {
    //用当前线程和以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);
    // 尝试插入尾节点的后面成为新的尾节点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //CAS操作将新节点设为尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //前继节点为空或者并发原因设置尾节点失败,进入enq方法
    enq(node);
    return node;
}

上面方法主要逻辑已注释,这里简单介绍下CAS操作
CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,返回成功,否则就什么都不做,返回失败。整个比较并替换的操作是一个原子操作。

enq方法

private Node enq(final Node node) {
    //死循环和compareAndSetTail方法,以CAS"自旋"方式,直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) {
            //队列为空则创建一个空节点作为头节点,这里因为并发原因使用CAS操作
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

到了这里,等待线程已经成功入队了。下面进入acquireQueued方法

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)) {
                //成功后将节点设为头节点,这时没有并发,所以不用CAS操作了
                setHead(node); 
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果p节点不是头节点,或者tryAcquire返回false,说明请求失败。  
            //那么先判断请求失败后node节点是否应该被阻塞,  
            //如果应该被阻塞,则阻塞node节点,当被唤醒后检测中断状态。
            //如果if为true说明,是中断的唤醒,将interrupted设为true
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. 如果当前节点是第二个节点,再次尝试加锁,成功后将节点设为头节点
  2. 当前节点不是第二节点,或者请求加锁失败,判断是否应该被阻塞,其实就是让有效前继节点的waitState为Signal
  3. 设置成功以后中断本节点的线程
  4. 方法返回的其实是interrupted的状态,调用程序可以自行决定是否处理中断
  5. finally中判断了failed状态,for循环可以看到,如果正常返回failed肯定是false,为 true说明发生异常,进行取消请求操作

shouldParkAfterFailedAcquire方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        //前继节点waitStatus为SIGNAL,就可以放心的阻塞了
        return true;
    if (ws > 0) {
        //跳过waitStatus为CANCLE的节点,这些都是无效前继节点了
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //前继节点waitStatus是0或者PROPAGATE
        //将前继节点的状态设为false 方法返回false,进入下一次循环
        //直到前继节点waitStatus为SIGNAL,然后就可以去阻塞线程了
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

当然shouldParkAfterFailedAcquire方法返回true以后,进入parkAndCheckInterrupt方法

parkAndCheckInterrupt

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//阻塞自己
    return Thread.interrupted();//当线程被唤醒后,代码从这里开始,返回中断状态
}
  1. 阻塞自己
  2. 返回中断状态,因为唤醒有两种方式:一种unpark, 一种线程interrupt

以上就是独占式获取锁方法的全部代码了,接下来看看如何释放

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;
}
  1. 尝试释放独占式锁
  2. 成功以后将头节点释放,唤醒头节点的后继节点
private void unparkSuccessor(Node node) {
    //如果waitStatus的值小于0,CAS设为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //如果后继节点不为空直接进行unpark
    //否则从尾部向前找不是取消状态的实际后继节点
    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);
}

释放独占锁成功,我们再回头看一下当头节点释放后,头节点的后继节点线程被unpark唤醒,parkAndCheckInterrupt方法会返回,acquireQueued方法内进入下一次循环,这次循环前继节点就是Head了,整个加锁解锁就形成闭环了。

acquireShared 共享式获取锁

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
  1. 尝试获取共享锁,当返回值大于等于0时,说明加锁成功
  2. 如果失败,执行doAcquireShared,加入等待队列
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) {//当返回值大于等于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);
    }
}

逻辑和acquireQueued类似
我们重点分析加锁成功后的setHeadAndPropagate方法

setHeadAndPropagate设置头节点并传播

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node); //将节点设置为头节点
    //如果propagate > 0说明还有资源,或者后继节点正在等待时
    //进行传播
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //下一节点在共享模式下等待时,进行唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

上面可以看到,多个if条件可能导致不必要的唤醒,但Doug Lea认为这些唤醒也是即将要发生的,可以忍受。
另外这个方法调用了doReleaseShared,就是唤醒后续等待节点。

releaseShared共享式释放锁

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            // 获取头节点对应的线程的状态
            int ws = h.waitStatus;
            // 如果头节点对应的线程是SIGNAL状态
            //则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。
            if (ws == Node.SIGNAL) {
                // 因为存在并发,所以CAS设置头节点状态为0,成功则进行唤醒后续节点
                //如果失败继续执行for循环。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒“头节点的下一个节点所对应的线程”。
                unparkSuccessor(h);
            }
            // 如果头节点对应的线程是空状态,将状态设置为PROPAGATE
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果头节点发生变化,则继续循环。否则,退出循环。
        if (h == head)                   // loop if head changed
            break;
    }
}

可以看到共享锁唤醒后继节点比独占锁复杂,因为共享锁是并发操作,所以使用循环和CAS保证并发的安全性。

你可能感兴趣的:(AbstractQueuedSynchronizer 源码分析)