Java并发编程笔记——J.U.C之locks框架:AQS共享功能原理剖析(4)

一、本章概述

AQS系列的前三个章节,我们通过ReentrantLock的示例,分析了AQS的独占功能。
本章将以CountDownLatch为例,分析AQS的共享功能。CountDownLatch,是J.U.C中的一个同步器类,可作为倒数计数器使用。

CountDownLatch示例
假设现在有3个线程,ThreadA、ThreadB、mainThread,CountDownLatch初始计数为1:
CountDownLatch switcher = new CountDownLatch(1);

线程的调用时序如下:

//ThreadA调用await()方法等待

//ThreadB调用await()方法等待

//主线程main调用countDown()放行

二、AQS共享功能的原理

1. 创建CountDownLatch

CountDownLatch的创建没什么特殊,调用唯一的构造器,传入一个初始计数值,内部实例化一个AQS子类:

CountDownLatch switcher = new CountDownLatch(1);

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    Sync(int count) {
        setState(count);
    }

可以看到,初始计数值count其实就是同步状态值,在CountDownLatch中,同步状态State表示CountDownLatch的计数器的初始大小。

2. ThreadA调用await()方法等待

CountDownLatch的await方法是响应中断的,该方法其实是调用了AQSacquireSharedInterruptibly方法:

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)//该方法尝试获取锁,由AQS子类实现
        doAcquireSharedInterruptibly(arg);
}

注意tryAcquireShared方法,该方法尝试获取锁,由AQS子类实现,其返回值的含义如下:

State资源的定义
小于0表示获取失败
等于0表示获取成功
大于0表示获取成功,且后继争用线程可能成功

CountDownLatch中的tryAcquireShared实现相当简单,当State值为0时,永远返回成功:
fb792b4649b917366613c250d3f18badea1.jpg

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
我们之前说了在 CountDownLatch中,同步状态State表示CountDownLatch的计数器的初始值, State==0时,表示无锁状态,且一旦State变为0,就永远处于无锁状态了, 此时所有线程在await上等待的线程都可以继续执行。这就是 共享锁的含义。
而在 ReentrantLock中, State==0时,虽然也表示无锁状态,但是只有一个线程可以重置State的值。这就是独占锁的含义。

好了,继续向下执行,ThreadA尝试获取锁失败后,会调用doAcquireSharedInterruptibly:注意:这里是自旋操作,后面或再次执行这段代码逻辑

1.doAcquireSharedInterruptibly方法分析开始

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);//重点:包装成共享节点,插入到等待队列
    boolean failed = true;
    try {
        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
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())    //检查是否需阻塞当前结点
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

1.1 分析上述代码:首先通过addWaiter方法,将ThreadA包装成共享结点,插入等待队列,插入完成后队列结构如下:

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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

fa8edc0be2a85b1a2f58c2433014bb24ae9.jpg

1.2 然后会进入自旋操作,先尝试获取一次锁,显然此时是获取失败的(主线程main还未调用countDown,同步状态State还是1)。
然后判断是否要进入阻塞(shouldParkAfterFailedAcquire):

//判断是否需要阻塞当前线程,注意:CLH队列的一个特点是:将当前结点的状态保存在它的前驱中
//前驱状态是【-1:等待唤醒】时,才会阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;    //前驱结点的状态
    if (ws == Node.SIGNAL)    //SIGNAL:后续结点需要被唤醒(这个状态说明当前结点的前驱结点将来会来唤醒我。我可以安心睡大觉了,哈哈(阻塞))
      
        return true;
    if (ws > 0) {    //CANCELED:取消(说明前驱结点(线程)因意外被中断/取消,需要将其从等待队列移除)
      
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
       
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    //对应独占功能来说,这里表示结点初始状态为0
    }
    return false;
}

1.3 好了,至此,ThreadA进入阻塞态,最终队列结构如下:
5ddf875e89b252667d84b9c624a416f35a3.jpg

3. ThreadB调用await()方法等待

流程和步骤2完全相同,调用后ThreadB也被加入到等待队列中:
debf4e1334c70a4ce2febd6327190c5e9bd.jpg

4. 主线程main调用countDown()放行

ThreadA和ThreadB调用了await()方法后都在等待了,现在主线程main开始调用countDown()方法,该方法调用后,ThreadA和ThreadB都会被唤醒,并继续往下执行,达到类似门栓的作用。

来看下countDown方法的内部:

public void countDown() {
    sync.releaseShared(1);    //将计数器值减少1(最小为0),为0,则释放所有等待线程
}

该方法内部调用了AQS的releaseShared方法,先尝试一次释放锁,tryReleaseShared方法是一个钩子方法,由CountDownLatch实现,当同步State状态值首次变为0时,会返回true:

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

a16ddb27c45f31d30fb5d690d89988a88f1.jpg

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;//将计数器值减少1
        if (compareAndSetState(c, nextc))//CAS设置状态值
            return nextc == 0;//如果state为0则返回true,否则返回false
    }
}
private void doReleaseShared() {
 
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))    //将头结点的等待状态置为0,表示将要唤醒后继节点
                    continue;            // loop to recheck cases
                unparkSuccessor(h);    //唤醒后继节点
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

先调用compareAndSetWaitStatus将头结点的等待状态置为0,表示将唤醒后续结点(ThreadA),成功后的等待队列结构如下:

private static final boolean compareAndSetWaitStatus(Node node,
                                                     int expect,
                                                     int update) {
    return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                    expect, update);
}

a3036ec07eec8333405bd250549cf0f28af.jpg

然后调用unparkSuccessor唤醒后继结点(ThreadA被唤醒后会从原阻塞处继续往下执行,这个在步骤5再讲):

//唤醒当前节点的后继节点(线程)
private void unparkSuccessor(Node node) {
    
    int ws = node.waitStatus;
    if (ws < 0)//SIGNAL=-1
        compareAndSetWaitStatus(node, ws, 0);    //预置当前节点的状态为0,表示后续节点即将被唤醒

    Node s = node.next;    //后继节点

    //正常情况下,会直接唤醒后继节点
    //但是如果后继节点处于1:CANCELLED状态时(说明被取消了),会从队尾开始,向前找到第一个未被CANCELLED的结点
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)    //从tail开始向前查找是为了考虑并发入队(enq)的情况
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)//唤醒结点
        LockSupport.unpark(s.thread);
}

此时,等待队列结构如下:
2a13359a2133de0f2a7df37e4454e635302.jpg

5. ThreadA从原阻塞处继续向下执行

ThreadA被唤醒后,会从原来的阻塞处继续向下执行:
由于是一个自旋操作,ThreadA会再次尝试获取锁,由于此时State同步状态值为0(无锁状态),所以获取成功。然后调用setHeadAndPropagate方法:注意:这里执行之前的自旋操作

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {    //包装成共享结点,插入等待队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        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
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())    //检查释放需要阻塞当前结点
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate方法把ThreadA结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:

//将当前结点 node设置为头结点,并尝试唤醒并释放后继共享结点
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
     //判断是否需要唤醒后继节点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;    //将当前结点设置为头结点
        if (s == null || s.isShared())
            doReleaseShared();    //释放并唤醒后继节点
    }
}

①将ThreadA变成头结点
28c8e59c9ba72a634f24e345cec19f3f01b.jpg

②调用doReleaseShared方法,释放并唤醒ThreadB结点

private void doReleaseShared() {
 
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))    //将头结点的等待状态置为0,表示将要唤醒后继节点
                    continue;            // loop to recheck cases
                unparkSuccessor(h);    //唤醒后继节点
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

d36ba0f628f76389196385bcbc3a5e1a845.jpg

6. ThreadB从原阻塞处继续向下执行

ThreadB被唤醒后,从原阻塞处继续向下执行,这个过程和步骤5(ThreadA唤醒后继续执行)完全一样。

setHeadAndPropagate方法把ThreadB结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);//将当前节点设置为头结点
     //判断是否需要唤醒后继节点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();//释放并唤醒后继节点
    }
}

①将ThreadB变成头结点
9cd3817458a23534f04b20df871a645c2f6.jpg

②调用doReleaseShared方法,释放并唤醒后继结点(此时没有后继结点了,则直接break):

private void doReleaseShared() {
 
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))    //将头结点的等待状态置为0,表示将要唤醒后继节点
                    continue;            // loop to recheck cases
                unparkSuccessor(h);    //唤醒后继节点
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

最终队列状态如下:
cdef12a5e2b8c04d984eaccfba2e6f2d096.jpg

三、总结

AQS的共享功能,通过钩子方法tryAcquireShared暴露,与独占功能最主要的区别就是:

共享功能的结点,一旦被唤醒,会向队列后部传播(Propagate)状态,以实现共享结点的连续唤醒。这也是共享的含义,当锁被释放时,所有持有该锁的共享线程都会被唤醒,并从等待队列移除。

参考链接

https://segmentfault.com/a/1190000015807573#articleHeader0

转载于:https://my.oschina.net/u/3995125/blog/3070777

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值