Java并发编程系列:CountDownLatch

CountDownLatch简介

CountDownLatch是基于AQS实现的一个线程同步工具,也称为闭锁。它的作用是让一个或者多个线程等待某个事件的发生。简单的理解为CountDownLatch有一个正数计数器,countDown方法对计数器做减操作,await方法等待计数器达到0。所有await的线程都会阻塞直到计数器为0或者等待线程中断或者超时。

CountDownLatch唯一的构造方法CountDownLatch(int count),当在闭锁上调用countDown()方法时,闭锁的计数器将减1,当闭锁计数器为0时,闭锁将打开,所有等待的线程将通过闭锁开始执行。

下面例子是让两个线程等待另外两个线程。

public class Work implements Runnable{
    private CountDownLatch downLatch;
    private String name;

    public Work(CountDownLatch downLatch, String name){
        this.downLatch = downLatch;
        this.name = name;
    }

    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.name + " done!");
        this.downLatch.countDown();
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(new Work(latch,"four")).start();
        new Thread(new Work(latch,"lc")).start();
        new Thread(() -> {
            try {
                latch.await();
            } catch (InterruptedException e) { }
            System.out.println("waiting over!");
        }).start();
        latch.await();

        System.out.println("all done");
    }

}
实现原理

下面分析CountDownLatch的实现原理,先从它的构造方法入手。

public CountDownLatch(int count) {
    this.sync = new Sync(count);
}

Sync继承了AQS,AQS中的state在这里表示计数值得大小,即等待几个线程的意思。

private static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
        setState(count);
    }
    int getCount() {
        return getState();
    }
}
await

在前面的例子中,我们知道了 await 的作用是等待计数器减为0,否则一直阻塞,直到超时或者中断。这里我们分析无参的方法。

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}
// AQS
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

如果线程已经中断,则抛出异常,否则,去获取锁,也就是判断state(计数器)是否为0。如果不为0,那么进入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) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这段代码和我们在AQS中分析acquireQueued方法逻辑基本是一样的,多了一步创建Node节点,并且Node是定义成共享的Node.SHARED,并且加入到同步队列中。被唤醒后重新尝试获取锁,不只设置自己为head,还需要通知其他等待的线程。尝试去获取锁,如果返回值大于0,表示获取成功,则去唤醒后面的节点。

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();
    }
}

setHead设置头节点后,取出下一个节点,如果也是共享类型,进行doReleaseShared释放操作。下个节点被唤醒后,重复上面的步骤,达到共享状态向后传播。

countDown

countDown是释放锁的过程,releaseShared在AQS中实现,tryReleaseShared是由CountDownLatch内部类Sync实现。

public void countDown() {
 sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

尝试释放锁,如果完全释放通知唤醒队列中的线程,完全释放指的是state减为0,同步队列中保存的是调用了await方法的线程。

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))
                    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;
    }
}

如果头结点的状态为SIGNAL,通过CAS置位0,并调用unparkSuccessor唤醒下个节点。被唤醒的节点状态会重置成0,在下一次循环中被设置成PROPAGATE状态,代表状态要向后传播。此方法的出口只有一个,即 h == head条件,当同步队列中没有可以唤醒的节点时,此条件成立。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    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);
}

这里需要注意在AQS中,我们分析到Head节点是持有锁的线程,在此处,Head节点是没有线程与它对应,Head节点后面的都是阻塞线程。unparkSuccessor方法的作用是寻找下一个可用节点(status<0),将它的状态置位0,并且唤醒。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值