Java并发——AQS源码解析

本文通过总结源码学习,来分析了解下AQS的工作原理

AQS是juc包锁实现的基础框架,研究juc包源码之前,AQS是必经之路
虽然说,平时项目中,我们几乎不会有自己去继承aqs实现锁的需要,但是通过源码了解aqs的机制和原理,有助于我们加深对各种锁的理解,以及出现问题时排查的思路

AbstractQueuedSynchronizer抽象队列同步器,CLH 锁

The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks. We instead use them for blocking synchronizers, but use the same basic tactic of holding some of the control information about a thread in the predecessor of its node. A "status" field in each node keeps track of whether a thread should block. A node is signalled when its predecessor releases.

双向FIFO等待队列,自旋锁,使用队列结点对象Node包装要获取锁的线程
AQS通过一个状态变量state,来标志当前线程持有锁的状态。
state = 0时代表没有持有锁,> 0 代表持有锁。
当队列中一个结点释放锁时,会唤醒后继阻塞的线程

内部类Node
static final class Node {
    /** 共享模式的Node */
    static final Node SHARED = new Node();
    /** 独占模式的Node */
    static final Node EXCLUSIVE = 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;

    /**
     * Returns true if node is waiting in shared mode.
     */
    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;
    }
}

如下图就是aqs同步队列中的一个节点结构,它有两个分别指向前后节点的指针,包含了当前线程thread,以及节点的状态waitStatus


Node

waitStatus解释
其实关于waitStatus,有点不好理解的是SIGNAL这个状态
我们先来看一下源码中是如何解释的

/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;

    /**
     * Status field, taking on only the values:
     *   SIGNAL:     The successor of this node is (or will soon be)
     *               blocked (via park), so the current node must
     *               unpark its successor when it releases or
     *               cancels. To avoid races, acquire methods must
     *               first indicate they need a signal,
     *               then retry the atomic acquire, and then,
     *               on failure, block.
     */
    volatile int waitStatus;

SIGNAL:表明后继结点需要被唤醒
该结点的后继结点已经阻塞或将被阻塞,所以当前结点必须唤醒它的后继结点当其释放锁或者取消时。

这里先通过注释有个初步概念,后续通过源码再来具体解释

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

在独占模式下,同一时刻只能有一个线程持有锁,其他线程都要等待

获取锁

acquire是个模板方法,先通过tryAcquire方法尝试获取锁,获取成功则修改aqs的状态state > 0,失败则加入等待队列中
tryAcquire是抽象方法需要子类实现

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

如果当前线程尝试获取锁失败,则将Node添加到等待队列中

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 尾结点不为空的话,在尾部添加该结点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾结点为null,说明队列此时为空,自旋插入该结点
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = 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;
            }
        }
    }
}

enq方法将会初始化队列,此时的aqs队列会变成如下形式



注意看初始化head和node的区别

compareAndSetHead(new Node())
Node node = new Node(Thread.currentThread(), mode);

head被初始化为一个空的node,它里面是没有包含线程信息的,而后面的node,会设置当前线程信息
也就是说aqs中真正的等待队列是不包括head的,后面黄色的node才是真正的等待获取锁的第一个节点
每次添加节点时,从链表尾部添加,然后让tail引用指向最后一个节点

添加到等待队列中,当前结点线程会自旋的去获取锁

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 拿到前驱结点
            final Node p = node.predecessor();
            // 前驱结点为head,说明当前结点是第一个,尝试获取锁
            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);
    }
}

这里我们可以看到,正如上面的图所示,获取锁时会先判断前驱节点是不是head,如果是head就去尝试获取锁,获取成功则自己变为head,这就是head的其中一个作用

获取锁失败,要判断当前结点线程是否应该被阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 判断前驱结点的状态
    int ws = pred.waitStatus;
    // 状态为SIGNAL,直接返回true
    if (ws == Node.SIGNAL)
        return true;
    // 状态大于0,前驱节点是取消状态
    if (ws > 0) {
        // 向前寻找非取消状态的node
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 状态为0或PROPAGATE,将其更新为signal
        // 会再次循环尝试获取锁,如果失败,在下一次循环就会阻塞当前线程
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

看到这里我们需要再回过头来理解下node中的waitStatus
该方法是判断当前线程是否应该被阻塞,可以看到,这里去判断前驱结点的状态,如果为SIGNAL,则直接返回true,调用LockSupport.park阻塞当前线程
然而注释中说的是SIGNAL表示后继结点需要被唤醒(unpark)
网上看到很多博文的解释是该状态表示要阻塞后继结点,说实话刚看到这个方法,的确会理解为这个意思
其实我们结合注释仔细想一下,如果前驱结点的状态为SIGNAL,那么就说明当前线程在之后是会被唤醒的,这样就可以放心的阻塞当前线程了,所以该方法通过判断前驱结点的状态,就是确保当前线程如果被阻塞了,它会在前驱结点释放锁时被唤醒

通过google相关资料,解释如下
图片截取自https://www.javarticles.com/2012/10/abstractqueuedsynchronizer-aqs.html

释放锁

和获取锁类似,同样是模板方法,需要子类实现抽象方法tryRelease

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        // head不为空且waitStatus不等于0
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

head不为空说明等待队列不为空
head的waitStatus不为0说明后继结点线程需要被唤醒,独占式模式下状态不为0其实就是SIGNAL

唤醒后继结点
注意这里传进来的node是head

 private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        // 将head的状态重置为0
        compareAndSetWaitStatus(node, ws, 0);

    // 如果后继结点为null或者被取消,则从tail开始向前,找到最后一个没有被取消的结点
    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;
    }
    // 唤醒head之后第一个需要被唤醒的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}
获取锁响应中断

前面获取锁的方法是忽略线程中断的,即使当前线程中断了,还是会在等待队列中等待被唤醒,aqs提供了acquireInterruptibly方法可以在获取锁时响应线程中断

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

尝试获取锁之前,如果当前线程已经中断了,那么直接throw异常

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 不是返回标记,而是直接抛出中断异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

可以看到此方法和acquireQueued区别就是,如果当前线程中断的话,直接抛出异常,而不是返回boolean类型的中断标记

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

看一下LockSupport.park方法的注释

 * <ul>
 * <li>Some other thread invokes {@link #unpark unpark} with the
 * current thread as the target; or
 *
 * <li>Some other thread {@linkplain Thread#interrupt interrupts}
 * the current thread; or
 *
 * <li>The call spuriously (that is, for no reason) returns.
 * </ul>

在其他线程unpark唤醒当前线程,或者interrupt中断当前线程时,该方法会返回
所以在等待队列中阻塞的线程,如果被其他线程中断,那么会返回,然后抛出中断异常,移出队列,线程销毁

获取锁响应中断及超时

除了单纯的响应线程中断以外,AQS另外还提供了可控制等待超时时间的方法tryAcquireNanos

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    // 计算deadline
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 每次循环计算需要等待的时间
            nanosTimeout = deadline - System.nanoTime();
            // 小于等于0,已经超时
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

就是响应中断的基础上,另外加了等待超时时间的控制
等待队列中的线程,被其他线程中断,或者等待超时,则移出等待队列

/**
 * The number of nanoseconds for which it is faster to spin
 * rather than to use timed park. A rough estimate suffices
 * to improve responsiveness with very short timeouts.
 */
static final long spinForTimeoutThreshold = 1000L;

1000纳秒是非常短的时间了,无法做到完全精确,程序执行也会耗费一定时间,所以这里粗略的估计在这个很短的时间内可以提高响应能力
<= spinForTimeoutThreshold 时将不再阻塞线程,直接再次自旋进行判断

共享式同步状态的获取和释放

共享模式,也就是同一时刻,可以有多个线程获取到锁,典型的应用就是读写锁
在其他线程没有获取到写锁时,读锁可以有多个线程获取到

获取锁

同独占模式一样,共享模式提供了抽象方法tryAcquireShared供子类实现
该方法不同于tryAcquire的地方是在于返回值为int类型,而不是boolean

/**
 * @return a negative value on failure; zero if acquisition in shared
 *         mode succeeded but no subsequent shared-mode acquire can
 *         succeed; and a positive value if acquisition in shared
 *         mode succeeded and subsequent shared-mode acquires might
 *         also succeed, in which case a subsequent waiting thread
 *         must check availability. (Support for three different
 *         return values enables this method to be used in contexts
 *         where acquires only sometimes act exclusively.)  Upon
 *         success, this object has been acquired.
 */
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

只截取了返回值说明,来看一下不同值代表的含义

  • 负数代表获取锁失败
  • 等于0代表获取锁成功,但是共享模式下的其他结点将无法成功获取锁
  • 大于0代表获取锁成功,共享模式下的其他结点也可能会成功获取锁

尝试获取锁失败,加入等待队列

private void doAcquireShared(int arg) {
    // 尝试获取锁失败,将线程添加到等待队列中
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 前驱结点为head,说明当前线程为等待队列中的第一个线程,尝试获取锁
            if (p == head) {
                int r = tryAcquireShared(arg);
                // 大于等于0,获取锁成功
                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);
    }
}

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    // tryAcquireShared结果大于0
    // 或者旧的head和新的head为空或状态被更新为SIGNAL
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 后继结点为空或是共享模式,则唤醒后继结点
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
释放锁
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 头不为空,且有结点在等待
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果状态为SIGNAL,则唤醒后继结点
            if (ws == Node.SIGNAL) {
                // cas重置头结点状态
                // 因为在releaseShared和setHeadAndPropagate方法都会调用该方法
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 共享模式下,将头结点状态设置为PROPAGATE
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果前面唤醒了后继结点,head将会被更新,则继续循环
        // head没有被更新,说明没有后继结点在阻塞,则跳出循环
        if (h == head)                   // loop if head changed
            break;
    }
}

同样的,共享模式也提供了获取锁响应中断,以及响应超时中断的方法

总结
  • AQS提供了尝试获取锁和释放锁的模板方法,子类根据具体场景实现
  • AQS中采用变量state来表示锁持有的状态,state大于0则表示持有锁
  • 有独占式和共享式两种,独占式同一时刻只能有一个线程持有锁,共享式的典型应用就是读写锁,写锁没有被持有时,读锁是可以有多个线程获取的
  • 两种模式均提供了,不响应中断,响应中断,响应超时中断的同步方法
  • head结点不是等待获取锁的线程,真正等待获取锁的结点是head后面开始的
  • 前驱结点的SIGNAL状态,表示后继结点可以被阻塞,当前驱结点释放锁的时候,会唤醒后继阻塞结点

作为一个“凡人”,有些地方还无法理解到Doug Lea大神的精髓和巧妙的设计,关于Java并发的研究才刚刚开始,继续努力

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容