并发编程——一文看尽CountDownLatch原理

这里写目录标题

  • 一级目录
    • 二级目录
      • 三级目录
      • CountDownLatch
        • 实际例子
        • 源码解析

一级目录

二级目录

三级目录

CountDownLatch

CountDownLatch是一个同步辅助类,允许一个线程等待其他线程执行完毕再继续执行。比如现在有一个线程0在执行任务A,执行任务过程中需要等待其他线程1、线程2等先完成任务B任务C的执行,那么线程0会暂停任务A的执行,线程0阻塞,等到线程1线程2执行完毕时线程0才会继续执行。

CountDownLatch使用AQS实现线程同步,初始化时要设置一个count,表示所需要等待执行完毕的线程数。每个线程执行完毕调用countDown()方法对count减一,所有线程执行完毕时,count = 0,原线程被唤醒将继续执行。

实际例子

新建一个Worker类:Worker类执行过程中睡眠3s再继续执行,执行完毕时在finally里执行countDown()方法。

public class Worker implements Runnable{
    String workerName;
    CountDownLatch countDownLatch;
    public Worker(String workerName,CountDownLatch countDownLatch){
        this.workerName = workerName;
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        System.out.println( workerName + " is working ...");
        try {
            // do something
            Thread.sleep(3000);
            System.out.println(workerName + "completed work!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            countDownLatch.countDown();
            //System.out.println("worker not countDown !");
        }
    }
}

CountDownLatch的count初始化为3,创建3个Worker并执行,最后调用latch.await()方法实现线程等待的效果
public class ThreadsTest {
    public static void main(String[] args) throws InterruptedException {
        int n = 3;
        CountDownLatch latch = new CountDownLatch(n);
        System.out.println("CountDownLatch test start......");
        for(int i = 1; i <= n; i++){
            String name = "worker-" + i;
            Worker worker = new Worker(name,latch);
            Thread thread = new Thread(worker);
            thread.start();
        }
        latch.await();
        System.out.println("CountDownLatch test end !");

    }
}
  • 查看执行效果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xr0fX2gk-1598112671761)(B6445B1B8C374CD6930E52E601111703)]

  • 如果我们不执行 latch.await()方法会怎样呢?这里先将该方法注释掉进行测试

// latch.await();

可以看到此时主线程新建3个worker线程后直接继续执行后面的操作,没有线程同步等待的效果。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g1j2Pr5b-1598112671763)(709AFA4800ED407BB8ACDAADF8468AC6)]

  • 如果worker线程不执行countDown()呢?将finally里面的countDown()方法注释掉:
finally {
            //countDownLatch.countDown();
            System.out.println("worker not countDown !");
        }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUeFriRk-1598112671764)(48FD6F90B30D44909A36B60CB9050427)]

由于没有进行countDown操作,latch.await()会一直阻塞着主线程,导致后面操作无法得到执行。

这个过程到底是如何实现的呢,剖析源码窥探其内部原理

源码解析

初始化

// CountDownLatch 类部分代码:

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
    
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

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

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

    }
    

源码的注释写的很清楚:Sync是CountdownLatch的同步控制器,而Sync是使用AQS实现的。
初始化AQS的同步变量state为线程数count,它是volatile修饰的状态变量。

countDown()方法:它调用了releaseShared()方法使得count–,达到线程调度的目的;根据源码注释可知当count = 0 时这个方法最终不会执行任何实际操作。

/**
    /**
     * Decrements the count of the latch, releasing all waiting threads if
     * the count reaches zero.
     *
     * 

If the current count is greater than zero then it is decremented. * If the new count is zero then all waiting threads are re-enabled for * thread scheduling purposes. * *

If the current count equals zero then nothing happens. */ public void countDown() { sync.releaseShared(1); }

接着看releaseShared()方法,它是父类AQS的方法,调用其实现类的tryReleaseShared()方法进行判断,如果tryReleaseShared返回true则释放共享锁,并返回true,否则返回false。

// AQS releaseShared()方法
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // 释放共享锁,后面再详细剖析该方法
            doReleaseShared();
            return true;
        }
        return false;
    }

先看CountDownLatch实现的tryReleaseShared()方法:

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            // for (;;)表示一直循环直至return
            for (;;) {
                // 获取同步状态变量state
                int c = getState();
                // 同步变量为0说明当前count = 0,所有线程都已执行完毕直接返回false
                if (c == 0)
                    return false;
                int nextc = c-1;
                // CAS 它是一个原子性的操作,利用CAS实现对state - 1的操作
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

通过源码分析可以知道,如果计数器count == 0,则返回false,这里的count指的就是同步状态变量state;如果不为0,则利用CAS的原子操作,执行count–操作,执行完毕返回 count == 0,也就是如果执行完毕count == 0 返回true,否则返回false。


当返回false时,所做的操作其实仅仅是对count执行减一的操作,而当返回true时,需要调用doReleaseShared()方法来释放共享锁。

分析doReleaseShared()方法之前先看一下 CountDownLatch的await()方法:

    // CountDownLatch 类 await()方法
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
    // AQS类 acquireSharedInterruptibly()方法
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 如果线程被中断抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

该方法实际的执行流程是先判断该线程是否被中断,如果被中断则抛出错误,如果没有被中断则调用 tryAcquireShared()尝试加共享锁的方法。如果当前 state == 0 说明未执行完毕的线程数为0,直接返回1,否则说明还有还有线程仍在执行中,返回 -1,继续执行doAcquireSharedInterruptibly()方法:

    // CountDownLatch 类 tryAcquireShared()方法
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }
    
    // AQS 类 doAcquireSharedInterruptibly()方法
    /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    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);
        }
    }
    
    /** 释放共享锁,唤醒等待队列中的线程
     * Release action for shared mode -- signals successor and ensures
     * propagation. (Note: For exclusive mode, release just amounts
     * to calling unparkSuccessor of head if it needs signal.)
     */
    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;
        }
    }

这里用到了AQS的阻塞等待队列和CLH锁,不在这进行描述。其实现过程简单概括就是:将当前线程放入等待队列中,当所有线程执行完毕count == 0 时再唤醒当前线程继续执行后续逻辑。

你可能感兴趣的:(java,并发编程)