AQS源码解析之常遇春求医

348 阅读5分钟

回忆

现代诗之我和AQS
AQS这个玩意,
每次看,都看不进去。
这次耐着性子终于看完,
记录一篇博客,便于后面忘了快速回忆起来。
开始吧,
用我的方式。

背景

背景交代: 

  • 资源: 神医谷胡青牛诊室 

  • 队列:看病排队 

  • 节点: 一个个看病的人(线程 + 状态信息)

开篇花絮: 

  • 这时候,武当俞岱岩来治腿伤,看了一下有人排队, 想到武当怎么说也是名门正派,我也排一下吧,这就是公平锁。 

  • 不一会,金毛狮王谢逊来了,威风凛凛地看了一眼诊室,恰好韦一笑刚走出诊室,狮王大大咧咧直接进了诊室,队伍里一阵义愤填膺,窃窃私语,明教法王就可以不排队么,这太不公平了。这就是非公平锁。

接下来,我以ReentrantLock的非公平锁为例,结合武侠背景,走读一下加锁和释放锁的流程。

常遇春被截心掌所伤,前来神医谷看病。正在往诊室走来~

获取锁

获取锁 ReentrantLock NonfairSync lock() 这里是ReentrantLock自己实现的前奏。

代码段1

final void lock() {
    if (compareAndSetState(0, 1))
        // 常遇春看一眼诊室门开着且没人, 于是
	// 直接进入诊室,并在门上贴上小纸条,常遇春在此,闲人勿进
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);  // MD,有人,于是陷入AQS内核
}

陷入AQS内核

代码段2

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

这里分三步: 

  1. tryAcquire 再看一眼,是否能抢进门,没人就冲进去,有人就算了 

  2. addWaiter 的确还有人,老实排到队伍的尾部(这时候,一小簇受伤的明教小兵过来了,也发现要排队,并正往队尾一路小跑,于是常遇春加快了而脚步) 

  3. acquireQueued 发呆,等着被前面的人叫醒或者冷醒(calling线程要在一个安全点等待)

第一步,tryAcquire (这个是AQS的用户自行实现对资源的获取)

代码段3

// NonfairSync类
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
// -- 
// NonfairSync 的父类 Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 当前诊室没人,无需排队
    if (c == 0) {
	// cas, 防止蜂拥进去
        if (compareAndSetState(0, acquires)) {
	    // 留下小纸条贴门上,来陪同的家人可以进来(可重入)
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 可重入
    else if (current == getExclusiveOwnerThread()) {
        // 重入后也要改变资源状态
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 没能冲进去,排队去吧
    return false;
}

回到代码段2,如果tryAcquire失败,则开始排队,addWaiter 老实排队,过分地话,神医胡青牛会发飙。于是一群没抢进门的人蜂拥开始排队。

代码段4

// AQS类中
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
		  // cas 冲刺排队
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
	  // 第一次冲刺排队失败,疯狂重试
    enq(node);
    return node;
}

代码段6

 第一次冲刺排队失败后的疯狂重试

// AQS类中
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;
            }
        }
    }
}

常遇春终于排好了队。接下来,干点啥打发一下时间呢,那时候还没有手机。睡着的话,万一轮到自己,后面这帮人肯定不会叫自己的。这时截心掌伤发作,哎呀~~~ 赶紧继续回到代码段2,acquireQueued,

代码段7

// AQS类中
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)) {
                // 常遇春:MD,终于轮到劳资了,一顿操作
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

代码段7中有2个要点,可以这样先这样去理解,在分析就容易了: 

  • shouldParkAfterFailedAcquire : 常遇春看了一下前面的兄弟,发现已经毒发身亡,摇了摇头,站到了这个人前面,并告诉前面的兄弟:“好兄弟,一会你出来的时候叫我一下,我伤有点重,想休息一下”
  •  parkAndCheckInterrupt:常遇春盘膝坐下,开始调养内息~ 循环过程中,如果排到自己,就返回到代码段2, 获取了资源,进入诊室。

代码段8

// AQS类中
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // pred是前面的兄弟,看看他的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         * 常遇春已经告诉过他了,待会叫自己,所以可以安心疗伤了
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         * 前面的兄弟已经毒发(或者其他状态,毒发好理解一点)
         * 常遇春摇了摇头,招呼了后面的人一起绕了过去,
         * 额,这个也毒发了,继续绕过去
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } 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);
    }
    return false;
}

代码段9

// AQS 类中
private final boolean parkAndCheckInterrupt() {
    // 常遇春开始疗伤,头顶正在升起一股蒸汽~
    LockSupport.park(this);

    // 常遇春:MD,疗伤被打断了,看看到我了没,回到代码段7 
    // 注意返回值,线程是否被中断过~,后面补中断
    return Thread.interrupted();
}

这里为啥要补中断呢,看一张图

线程park后,可能会被唤醒, 但是现在已经陷入AQS中(在这个无限循环里,不方便跳出来),醒来也是在循环里,所以记录一下中途被人叫醒过。 

 至此,常遇春已经拿到号,进入了诊室。

胡青牛以张无忌非明教中人,不予治疗,常遇春舍命替换,都是后话。 这里剧情稍微篡改一下。

好了,到此,锁的获取已经结束,下面开始说锁的释放。 

释放锁

常遇春拿着药,骂骂咧咧走出来道:下一位,下一位! 

后来常遇春被张无忌以猛药治好,但是损失了几十年阳寿,也是后话了~ 

 时间稍微往回拉,我们看看刚才,看看常遇春是怎么被人叫醒的~

代码段10

// ReentrantLock
public void unlock() {
    sync.release(1);
}

// AQS类
public final boolean release(int arg) {
    // 韩千叶走出诊室,一脸惆怅,他开始释放诊室资源
    if (tryRelease(arg)) {
        // 资源有空闲(诊室空出来了),可以叫后面的兄弟了
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  // 叫醒后面的兄弟
        return true;
    }
    return false;
}

// ReentrantLock Sync类
protected final boolean tryRelease(int releases) {
    // 释放资源
    int c = getState() - releases;

	 // 非当前线程,无资格释放
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    
	  // free的意思,是资源有空闲,可以叫后面的兄弟进来了
    // 在ReentrantLock这种排它锁里,只有state为0,才表示有空闲
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }

	  // 获取资源的就当前一个,所以是线程安全的,不需要cas
    setState(c);
    return free;
}

那如何叫醒后面的兄弟呢, 在unparkSuccessor里。

代码段11

// AQS
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     * 这里,嗯,感觉没啥用~
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     * 看一眼后面的兄弟
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) { 
        // 已经毒发了,或者被人叫走了
        s = null;

	// 那就从后往前找一遍,看看新来的,一直找到距离head最近的那个人 
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }

    // 终于找到了下一个可以进诊室的人了, 你进去吧,喂喂喂
    if (s != null)
        LockSupport.unpark(s.thread);
}

 接下来就可以回到 代码段9 以及 代码段7, 就看到常遇春怎么醒来的。