当前位置: 首页 > news >正文

《深入浅出 Java Concurrency》—锁机制(五) 闭锁 (CountDownLatch)

转自:http://www.blogjava.net/xylz/archive/2010/07/09/325612.html

此小节介绍几个与锁有关的有用工具。

闭锁(Latch)

闭锁(Latch):一种同步方法,可以延迟线程的进度直到线程到达某个终点状态。通俗的讲就是,一个闭锁相当于一扇大门,在大门打开之前所有线程都被阻断,一旦大门打开所有线程都将通过,但是一旦大门打开,所有线程都通过了,那么这个闭锁的状态就失效了,门的状态也就不能变了,只能是打开状态。也就是说闭锁的状态是一次性的,它确保在闭锁打开之前所有特定的活动都需要在闭锁打开之后才能完成。

CountDownLatch 是JDK 5+里面闭锁的一个实现,允许一个或者多个线程等待某个事件的发生。CountDownLatch 有一个正数计数器,countDown 方法对计数器做减操作,await 方法等待计数器达到0。所有await 的线程都会阻塞直到计数器为0或者等待线程中断或者超时。

CountDownLatch 的API如下。

  • public void await() throws InterruptedException
  • public boolean await(long timeout, TimeUnit unit) throws InterruptedException
  • public void countDown()
  • public long getCount()

其中getCount() 描述的是当前计数,通常用于调试目的。

下面的例子中描述了闭锁的两种常见的用法。

package xylz.study.concurrency.lock;

import java.util.concurrent.CountDownLatch;

public class PerformanceTestTool {

    public long timecost(final int times, final Runnable task) throws InterruptedException {
        if (times <= 0) throw new IllegalArgumentException();
        final CountDownLatch startLatch = new CountDownLatch(1);
        final CountDownLatch overLatch = new CountDownLatch(times);
        for (int i = 0; i < times; i++) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        startLatch.await();
                        //
                        task.run();
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    } finally {
                        overLatch.countDown();
                    }
                }
            }).start();
        }
        //
        long start = System.nanoTime();
        startLatch.countDown();
        overLatch.await();
        return System.nanoTime() - start;
    }

}

在上面的例子中使用了两个闭锁,第一个闭锁确保在所有线程开始执行任务前,所有准备工作都已经完成,一旦准备工作完成了就调用startLatch.countDown() 打开闭锁,所有线程开始执行。第二个闭锁在于确保所有任务执行完成后主线程才能继续进行,这样保证了主线程等待所有任务线程执行完成后才能得到需要的结果。在第二个闭锁当中,初始化了一个N次的计数器,每个任务执行完成后都会将计数器减一,所有任务完成后计数器就变为了0,这样主线程闭锁overLatch拿到此信号后就可以继续往下执行了。

根据前面的happend-before法则 可以知道闭锁有以下特性:

内存一致性效果:线程中调用  countDown()   之前的操作  happen-before   紧跟在从另一个线程中对应  await()   成功返回的操作。

在上面的例子中第二个闭锁相当于把一个任务拆分成N份,每一份独立完成任务,主线程等待所有任务完成后才能继续执行。这个特性在后面的线程池框架中会用到,其实FutureTask 就可以看成一个闭锁。后面的章节还会具体分析FutureTask 的。

 

同样基于探索精神,仍然需要“窥探”下CountDownLatch 里面到底是如何实现await*countDown 的。

首先,研究下await() 方法。内部直接调用了AQSacquireSharedInterruptibly(1)

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

前面一直提到的都是独占锁(排它锁、互斥锁),现在就用到了另外一种锁,共享锁。

所谓共享锁是说所有共享锁的线程共享同一个资源,一旦任意一个线程拿到共享资源,那么所有线程就都拥有的同一份资源。也就是通常情况下共享锁只是一个标志,所有线程都等待这个标识是否满足,一旦满足所有线程都被激活(相当于所有线程都拿到锁一样)。这里的闭锁CountDownLatch 就是基于共享锁的实现。

闭锁中关于AQStryAcquireShared 的实现是如下代码(java.util.concurrent.CountDownLatch.Sync.tryAcquireShared ):

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

在这份逻辑中,对于闭锁而言第一次await时tryAcquireShared应该总是-1,因为对于闭锁CountDownLatch 而言state 的值就是初始化的count 值。这也就解释了为什么在countDown 调用之前闭锁的count 总是>0。

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())
                break;
        }
    } catch (RuntimeException ex) {
        cancelAcquire(node);
        throw ex;
    }
    // Arrive here only if interrupted
    cancelAcquire(node);
    throw new InterruptedException();
}

上面的逻辑展示了如何通过await 将所有线程串联并挂起,直到被唤醒或者条件满足或者被中断。整个过程是这样的:

  1.  
    1. 将当前线程节点以共享模式加入AQSCLH 队列中(相关概念参考这里 和这里 )。进行2。
    2. 检查当前节点的前任节点,如果是头结点并且当前闭锁计数为0就将当前节点设置为头结点,唤醒继任节点,返回(结束线程阻塞)。否则进行3。
    3. 检查线程是否该阻塞,如果应该就阻塞(park),直到被唤醒(unpark)。重复2。
    4. 如果2、3有异常就抛出异常(结束线程阻塞)。

这里有一点值得说明下,设置头结点并唤醒继任节点setHeadAndPropagate 。由于前面tryAcquireShared 总是返回1或者-1,而进入setHeadAndPropagate 时总是propagate>=0 ,所以这里propagate==1 。后面唤醒继任节点操作就非常熟悉了。

private void setHeadAndPropagate(Node node, int propagate) {
    setHead(node);
    if (propagate > 0 && node.waitStatus != 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            unparkSuccessor(node);
    }
}

从上面的所有逻辑可以看出countDown 应该就是在条件满足(计数为0)时唤醒头结点(时间最长的一个节点),然后头结点就会根据FIFO队列唤醒整个节点列表(如果有的话)。

CountDownLatchcountDown 代码中看到,直接调用的是AQSreleaseShared(1) ,参考前面的知识,这就印证了上面的说法。

tryReleaseShared 中正是采用CAS操作减少计数(每次减-1)。

public boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

整个CountDownLatch 就是这个样子的。其实有了前面原子操作和AQS 的原理及实现,分析CountDownLatch 还是比较容易的。

相关文章:

  • 《深入浅出 Java Concurrency》—锁机制(六) CyclicBarrier
  • PHPMySQL 语法
  • 《深入浅出 Java Concurrency》—锁机制(七) 信号量 (Semaphore)
  • jquery easyui datagrid 动态 加载列
  • 《深入浅出 Java Concurrency》—锁机制(八) 读写锁 (ReentrantReadWriteLock) (1)
  • Spring AOP
  • 《深入浅出 Java Concurrency》—锁机制(九) 读写锁 (ReentrantReadWriteLock) (2)
  • 《深入浅出 Java Concurrency》—锁机制(十) 锁的一些其它问题
  • Unix高级编程之文件IO
  • java集合框架学习—ArrayList的实现原理
  • 不等式证明
  • java集合框架学习—HashMap的实现原理
  • PHP 错误 系列:编码格式错误解决
  • java集合框架学习—HashSet的实现原理
  • 【腾讯优测干货分享】Android内存泄漏的简单检查与分析方法
  • JavaScript-如何实现克隆(clone)函数
  • 【刷算法】从上往下打印二叉树
  • 〔开发系列〕一次关于小程序开发的深度总结
  • CSS中外联样式表代表的含义
  • ES2017异步函数现已正式可用
  • ES6系统学习----从Apollo Client看解构赋值
  • HTML-表单
  • iOS仿今日头条、壁纸应用、筛选分类、三方微博、颜色填充等源码
  • Java 内存分配及垃圾回收机制初探
  • linux安装openssl、swoole等扩展的具体步骤
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • vue:响应原理
  • Wamp集成环境 添加PHP的新版本
  • 理解IaaS, PaaS, SaaS等云模型 (Cloud Models)
  • 如何合理的规划jvm性能调优
  • 实战|智能家居行业移动应用性能分析
  • 数组大概知多少
  • 数组的操作
  • 微信小程序设置上一页数据
  • 一道面试题引发的“血案”
  • UI设计初学者应该如何入门?
  • 曾刷新两项世界纪录,腾讯优图人脸检测算法 DSFD 正式开源 ...
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • $GOPATH/go.mod exists but should not goland
  • (003)SlickEdit Unity的补全
  • (6)STL算法之转换
  • (C++20) consteval立即函数
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (大众金融)SQL server面试题(1)-总销售量最少的3个型号的车及其总销售量
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (十八)用JAVA编写MP3解码器——迷你播放器
  • (转) Face-Resources
  • (转)母版页和相对路径
  • (转载)从 Java 代码到 Java 堆
  • **PHP二维数组遍历时同时赋值
  • .libPaths()设置包加载目录
  • .NET MAUI学习笔记——2.构建第一个程序_初级篇
  • .NET构架之我见