java countdownlatch

1.背景:

  • countDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue

  • 存在于java.util.cucurrent包下

2.概念

  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行

  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了

  • 只能一次性使用(不能reset);主线程阻塞;某个线程中断将永远到不了屏障点,所有线程都会一直等待

3.源码

  countDownLatch类中只提供了一个构造器:

//参数count为计数值
public CountDownLatch(int count) {  };  
  类中有三个方法是最重要的:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  

3.1 源码分析

await() 源码解析:

  await()源码如下:

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

  调用的是AQSacquireSharedInterruptibly(int arg)方法:

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 如果被中断,抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取同步状态
        if (tryAcquireShared(arg) < 0)
            // 获取同步状态失败,自旋
            doAcquireSharedInterruptibly(arg);
    }

  首先,通过tryAcquireShared(arg)尝试获取同步状态,具体的实现被Sync重写了,查看源码:

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

  如果同步状态的值为0,获取成功。这就是CountDownLatch的机制,尝试获取latch的线程只有当latch的值减到0的时候,才能获取成功。如果获取失败,则会调用AQS的doAcquireSharedInterruptibly(int arg)函数自旋,尝试挂起当前线程:

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 将当前线程加入同步队列的尾部
        final Node node = addWaiter(Node.SHARED);
        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
                        return;
                    }
                }
                // 如果当前节点的前驱不是头结点,尝试挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

  这里,调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() 挂起当前线程

countDown() 源码解析:

  countDown()源码如下:

    public void countDown() {
        sync.releaseShared(1);
    }

  调用的是AQSreleaseShared(int arg)方法:

    public final boolean releaseShared(int arg) {
        // 尝试释放同步状态
        if (tryReleaseShared(arg)) {
            // 如果成功,进入自旋,尝试唤醒同步队列中头结点的后继节点
            doReleaseShared();
            return true;
        }
        return false;
    }

  首先,通过tryReleaseShared(arg)尝试释放同步状态,具体的实现被Sync重写了,源码:

    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            // 同步状态值减1
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }

  如果同步状态值减到0,则释放成功,进入自旋,尝试唤醒同步队列中头结点的后继节点,调用的是AQSdoReleaseShared()函数:

    private void doReleaseShared() {
        for (;;) {
            // 获取头结点
            Node h = head;
            if (h != null && h != tail) {
                // 获取头结点的状态
                int ws = h.waitStatus;
                // 如果是SIGNAL,尝试唤醒后继节点
                if (ws == Node.SIGNAL) {
                    if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 唤醒头结点的后继节点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

  这里调用了unparkSuccessor(h)去唤醒头结点的后继节点

如何唤醒所有调用 await() 等待的线程:

  回到线程被挂起的地方,也就是doAcquireSharedInterruptibly(int arg)方法中:

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 将当前线程加入同步队列的尾部
        final Node node = addWaiter(Node.SHARED);
        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
                        return;
                    }
                }
                // 如果当前节点的前驱不是头结点,尝试挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

  该方法里面,通过调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()将线程挂起。当头结点的后继节点被唤醒后,线程将从挂起的地方醒来,继续执行,因为没有return,所以进入下一次循环。此时,获取同步状态成功,执行setHeadAndPropagate(node, r)。查看源码:

    // 如果执行这个函数,那么propagate一定等于1
    private void setHeadAndPropagate(Node node, int propagate) {
        // 获取头结点
        Node h = head;
        // 因为当前节点被唤醒,设置当前节点为头结点
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // 获取当前节点的下一个节点
            Node s = node.next;
            // 如果下一个节点为null或者节点为shared节点
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

  这里,当前节点被唤醒,首先设置当前节点为头结点。如果当前节点的下一个节点是shared节点,调用doReleaseShared(),源码:

    private void doReleaseShared() {
        // 自旋
        for (;;) {
            // 获取头结点,也就是当前节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果head没有改变,则调用break退出循环
            if (h == head)
                break;
        }
    }

  首先,注意if (h == head) break; 这里每次循环的时候判断head头结点有没有改变,如果没有改变则退出循环。因为只有当新的节点被唤醒之后,新节点才会调用setHead(node)设置自己为头结点,头结点才会改变。其次,注意if (h != null && h != tail) 这个判断,保证队列至少要有两个节点(包括头结点在内)
  如果队列中有两个或以上个节点,那么检查局部变量h的状态:
  如果状态为SIGNAL,说明h的后继节点是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)和unparkSuccessor(h)绑定在了一起。说明了只要head成功的从SIGNAL修改为0,那么head的后继节点对应的线程将会被唤醒
     如果状态为0,说明h的后继节点对应的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到
  如果状态为PROPAGATE,直接判断head是否变化

4.例子

    //创建初始化3个线程的线程池
    private ExecutorService threadPool     = Executors.newFixedThreadPool(3);
    //保存每个学生的平均成绩
    private ConcurrentHashMap<String, Integer> map            = new ConcurrentHashMap<>();
    private CountDownLatch countDownLatch = new CountDownLatch(3);

    private void count() {
        for (int i = 0; i < 3; i++) {
            threadPool.execute(() -> {
                //计算每个学生的平均成绩,代码略()假设为60~100的随机数
                int score = (int) (Math.random() * 40 + 60);
                try {
                    Thread.sleep(Math.round(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                map.put(Thread.currentThread().getName(), score);
                System.out.println(Thread.currentThread().getName() + "同学的平均成绩为" + score);
                countDownLatch.countDown();
            });
        }
        this.run();
        threadPool.shutdown();
    }

    @Override
    public void run() {
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int result = 0;
        Set<String> set = map.keySet();
        for (String s : set) {
            result += map.get(s);
        }
        System.out.println("三人平均成绩为:" + (result / 3) + "分");
    }

    public static void main(String[] args) throws InterruptedException {
        long now = System.currentTimeMillis();
        CyclicBarrierTest cb = new CyclicBarrierTest();
        cb.count();
        Thread.sleep(100);
        long end = System.currentTimeMillis();
        System.out.println("消耗时间(毫秒):" + (end - now));
    }

 

CountDownLatch和CyclicBarrier区别:

  1. CountDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次

  2. CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用
 
  3. CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true

  4. CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。某线程中断CyclicBarrier会抛出异常,避免了所有线程无限等待
 
  CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行

  CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行

  对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待

  CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行

 
 

posted on 2022-01-20 16:56  胡子就不刮  阅读(145)  评论(0编辑  收藏  举报

导航