014-线程同步辅助类-CountDownLatch

一、概述  

  CountDownLatch是JAVA提供在java.util.concurrent包下的一个辅助类,指定的一个或多个线程等待其他线程执行完成后执行。

  能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。

1.1、主要方法

// 构造器,必须指定一个大于零的计数
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

// 线程阻塞,直到计数为0的时候唤醒;可以响应线程中断退出阻塞
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// 线程阻塞一段时间,如果计数依然不是0,则返回false;否则返回true
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

// 计数-1
public void countDown() {
    sync.releaseShared(1);
}

// 获取计数
public long getCount() {
    return sync.getCount();
}

    它的内部有一个辅助的内部类:sync

  countDownLath无法被重置,循环栅栏可以。

  循环栅栏是要执行的线程增加,countDownLatch是执行完成的线程增加。

1.2、实现原理

  同ReentrantLock一样,依然是借助AQS的双端队列,来实现原子的计数-1,线程阻塞和唤醒

  参看:013-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架

1. 计数器的初始化

  CountDownLatch内部实现了AQS,并覆盖了tryAcquireShared()tryReleaseShared()两个方法,下面说明干嘛用的

  通过前面的使用,清楚了计数器的构造必须指定计数值,这个直接初始化了 AQS内部的state变量

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

  后续的计数-1/判断是否可用都是基于state进行的

2. countDown() 计数-1的实现

// 计数-1
public void countDown() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 首先尝试释放锁
        doReleaseShared();
        return true;
    }
    return false;
}

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0) //如果计数已经为0,则返回失败
            return false;
        int nextc = c-1;
        // 原子操作实现计数-1
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

// 唤醒被阻塞的线程
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) { // 队列非空,表示有线程被阻塞
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
            // 头结点如果为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) // 没有线程被阻塞,直接跳出
            break;
    }
}

countDown内部实现流程:

  1. 尝试释放锁tryReleaseShared,实现计数-1
  • 若计数已经小于0,则直接返回false
  • 否则执行计数(AQS的state)减一
  • 若减完之后,state==0,表示没有线程占用锁,即释放成功,然后就需要唤醒被阻塞的线程了
  1. 释放并唤醒阻塞线程 doReleaseShared
  • 如果队列为空,即表示没有线程被阻塞(也就是说没有线程调用了 CountDownLatch#wait()方法),直接退出
  • 头结点如果为SIGNAL, 则依次唤醒头结点下个节点上关联的线程,并出队

注意:即CountDownLatch计数为0之后,所有被阻塞的线程都会被唤醒,且彼此相对独立,不会出现独占锁阻塞的问题

3. await() 阻塞等待计数为0

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

// 计数为0时,表示获取锁成功
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

// 阻塞,并入队
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) {
                    // 获取锁成功,设置队列头为node节点
                    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. 判断state计数是否为0,不是,则直接放过执行后面的代码
  2. 大于0,则表示需要阻塞等待计数为0
  3. 当前线程封装Node对象,进入阻塞队列
  4. 然后就是循环尝试获取锁,直到成功(即state为0)后出队,继续执行线程后续代码

1.3、使用注意

  • 在创建实例时,必须指定初始的计数值,且应大于0
  • 必须有线程中显示的调用了countDown()计数-1方法;必须有线程显示调用了await()方法(没有这个就没有必要使用CountDownLatch了)
  • 由于await()方法会阻塞到计数为0,如果在代码逻辑中某个线程漏掉了计数-1,导致最终计数一直大于0,直接导致死锁了;
  • 鉴于上面一点,更多的推荐 await(long, TimeUnit)来替代直接使用await()方法,至少不会造成阻塞死只能重启的情况
  • 允许多个线程调用await方法,当计数为0后,所有被阻塞的线程都会被唤醒

1.4、使用场景

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行。
  • 确保某个服务在其依赖的所有其他服务都已启动后才启动。
  • 等待知道某个操作的所有者都就绪在继续执行。

1.5、示例

  有5个运动员赛跑,为了公平所有运动员需要同时起跑。同时,当最后一个运动员跑完之后,比赛结束。

    public static void main(String[] args) {
        //所有线程阻塞,然后统一开始
        CountDownLatch begin = new CountDownLatch(1);
        //主线程阻塞,直到所有分线程执行完毕
        CountDownLatch end = new CountDownLatch(5);
        for(int i = 0; i < 5; i++){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        begin.await();
                        System.out.println(LocalDateTime.now()+":"+Thread.currentThread().getName() + " 起跑");
                        Thread.sleep(new Random().nextInt(2000));
                        System.out.println(LocalDateTime.now()+":"+Thread.currentThread().getName() + " 到达终点");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    finally {
                        end.countDown();
                    }

                }
            });

            thread.start();
        }

        try {
            System.out.println(LocalDateTime.now()+":"+"1秒后统一开始");
            Thread.sleep(1000);
            begin.countDown();

            end.await();
            System.out.println(LocalDateTime.now()+":"+"停止比赛");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

结果

2019-02-12T16:51:38.903:1秒后统一开始
2019-02-12T16:51:39.904:Thread-1 起跑
2019-02-12T16:51:39.904:Thread-4 起跑
2019-02-12T16:51:39.904:Thread-3 起跑
2019-02-12T16:51:39.904:Thread-2 起跑
2019-02-12T16:51:39.904:Thread-0 起跑
2019-02-12T16:51:40.035:Thread-0 到达终点
2019-02-12T16:51:40.484:Thread-2 到达终点
2019-02-12T16:51:41.087:Thread-4 到达终点
2019-02-12T16:51:41.215:Thread-3 到达终点
2019-02-12T16:51:41.287:Thread-1 到达终点
2019-02-12T16:51:41.288:停止比赛

原文地址:https://www.cnblogs.com/bjlhx/p/10365871.html

时间: 2024-10-15 04:46:49

014-线程同步辅助类-CountDownLatch的相关文章

JAVA线程同步辅助类CountDownLatch

一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 用给定的计数 初始化 CountDownLatch.由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞.之后,会释放所有等待的线程,await 的所有后续调用都将立即返回.这种现象只出现一次——计数无法被重置.如果需要重置计数,请考虑使用 CyclicBarrier. CountDownLatch 是一个通用同步工具,它有很多用途.将计数 1 初始化的 Count

第三章线程同步辅助类

Java 7 并发编程实战手册目录 代码下载(https://github.com/Wang-Jun-Chao/java-concurrency) 第三章线程同步辅助类 3.1简介 ?信号量(Semaphore):是一种计数器,用来保护一个或者多个共享资源的访问.它是并发编程的一种基础工具,大多数编程语言都提供了这个机制. ? CountDownLatch:是Java语言提供的同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许线程一直等待. ? CyclicBarrier也是Java语言

[笔记][Java7并发编程实战手册]第三章-线程同步辅助类-概要

[笔记][Java7并发编程实战手册]系列目录 有点着急了,没有太注重质量,自己也没有理解透,从本章起,读书和随笔笔记的质量会更好. 第三章 在本章中,我们将学习: 资源的并发访问控制 资源的多副本的并发访问控制 等待多个并发事件的完成 在集合点的同步 并发阶段任务的运行 并发阶段任务中的阶段交换 并发任务间的数据交换 回顾 在第二章中主要学习了以下接口 synchronized关键字 Lock接口以及实现类,如ReentrantLock.ReentrantReadWriteLock中的Read

同步辅助类CountDownLatch用法

CountDownLatch是一个同步辅助类,犹如倒计时计数器,创建对象时通过构造方法设置初始值,调用CountDownLatch对象的await()方法则使当前线程处于等待状态,调用countDown()方法就将计数器减1,当计数到达0时,则所有等待线程全部开始执行.它提供的常用方法: public CountDownLatch(int count); //构造方法参数指定了计数的次数 public void countDown(); //当前线程调用此方法,则计数减一 public void

线程同步机制(二)-- 线程同步辅助类

我们在线程同步机制(一)--Synchronized和Lock简要介绍中学习了同步和临界区的概念,并且讨论了多个并发任务共享一个资源时的同步情况.访问共享资源的代码块叫临界区. 我们在线程同步机制(一)--Synchronized和Lock简要介绍中学习了一下内容: synchronized关键字 Lock接口及其实现类,如ReentrantLock,ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock 本章我们将学习如

JAVA线程同步辅助类CyclicBarrier循环屏障

CyclicBarrier是一个同步辅助类,主要作用是让一组线程互相等待,知道都到达一个公共障点,在一起走.在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用.因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier. CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次.若在继续所有参与线程之前更新共享状态,此屏

java并发之同步辅助类CountDownLatch

CountDownLatch 含义: CountDownLatch可以理解为一个计数器在初始化时设置初始值,当一个线程需要等待某些操作先完成时,需要调用await()方法.这个方法让线程进入休眠状态直到等待的所有线程都执行完成.每调用一次countDown()方法内部计数器减1,直到计数器为0时唤醒.这个可以理解为特殊的CyclicBarrier.线程同步点比较特殊,为内部计数器值为0时开始. 方法:核心方法两个:countDown()和await()countDown():使CountDown

线程同步工具CountDownLatch的使用

CountDownlatch,是一种Sychronizer,它可以延迟线程的进度直到线程的进度到线程到达终止状态. 它本身而言是Java并发包中非常有用的一个类,它可以让某些任务完成以后再继续运行下面的内容,每个任务本身执行完毕后让计数器减一,直到计数器清零后,以下的内容才可以继续运行,否则将阻塞等待. CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行.假如我们这个想要继续往下执行的任务调用一个CountDownL

线程同步工具CountDownLatch

CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行.假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止. CountDownLatch类有3个基本元素: 初始值决定C