热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

(JUC下典型的类)Java并发包中线程同步器

Java并发包中线程同步器CountDownLatchCountDownLatch与join方法的区别CountDownLatch中的方法介绍回环屏障CyclicBarrierCy

Java 并发包中线程同步器

  • CountDownLatch
    • CountDownLatch 与 join 方法的区别
    • CountDownLatch 中的方法介绍
  • 回环屏障 CyclicBarrier
    • CyclicBarrier 中几个重要方法
  • 信号量 Semaphore
    • Semaphore 主要方法
  • 总结


CountDownLatch

在日常开发中经常会遇到需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。在 CountDownLatch 出现之前一般都使用线程 join() 方法来实现这一点,但是 join 方法不够灵活,不能满足不同场景的需要,所以 JDK 开发提供了 CountDownLatch 这个类,使用 CountDownLatch 代码如下:

// 计数器:判断线程池的任务是否已经全部执行完import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;public class CountDownLatchDemo1 {public static void main(String[] args) throws InterruptedException {// 创建计数器CountDownLatch countDownLatch &#61; new CountDownLatch(5);// 创建新线程执行任务for (int i &#61; 0; i < 5; i&#43;&#43;) {new Thread(() -> {Thread currThread &#61; Thread.currentThread();System.out.println(currThread.getName() &#43; "开始执行");// 线程执行所用时间int runTime &#61; (1 &#43; new Random().nextInt(5));try {TimeUnit.SECONDS.sleep(runTime);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(currThread.getName() &#43; "执行完成&#xff0c;用时" &#43; runTime);//计数器-1countDownLatch.countDown();},"线程" &#43; i&#43;"-> ").start();}countDownLatch.await(); // 阻塞等待&#xff0c;直到所有线程执行完System.out.println("执行结果");}
}

在如上代码中&#xff0c;创建了一个 CountDownLatch 实例&#xff0c;用 for 循环创建 5 个线程&#xff0c;所以给构造函数传递参数为 5。主线程调用 countDownLatch.await() 方法后会被阻塞。子线程执行完毕后调用 countDownLatch.countDown() 方法让countDownLatch 内部的计数器减 1&#xff0c;所有子线程执行完毕后并调用 countDown() 方法后计数器会变为 0&#xff0c;这时候主线程的 await() 方法才会返回。

以上代码是用直接循环创建 5 个线程实现的&#xff0c;其实在项目实践中一般都避免直接操作线程&#xff0c;而是使用 ExecutorService 线程池来管理&#xff0c;使用 ExecutorService 时传递的参数是 Runnable 或者 Callable 对象&#xff0c;这时候你就没有办法调用这些线程的 join() 方法&#xff0c;这就需要选择使用 CountDownLatch 了&#xff0c;将上面代码修改如下&#xff1a;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class CountDownLatchDemo2 {// 创建计数器private static CountDownLatch countDownLatch &#61; new CountDownLatch(5);public static void main(String[] args) throws InterruptedException {// 创建线程池ExecutorService executorService &#61; Executors.newFixedThreadPool(5);for (int i &#61; 0; i < 5; i&#43;&#43;) {executorService.submit(new Runnable() {&#64;Overridepublic void run() {Thread currThread &#61; Thread.currentThread();System.out.println(currThread.getName() &#43; "开始执行");// 线程执行所用时间int runTime &#61; (1 &#43; new Random().nextInt(5));try {TimeUnit.SECONDS.sleep(runTime);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(currThread.getName() &#43; "执行完成&#xff0c;用时" &#43; runTime);//计数器-1countDownLatch.countDown();}});}countDownLatch.await(); // 阻塞等待&#xff0c;直到所有线程执行完System.out.println("执行结果");}
}

CountDownLatch 与 join 方法的区别

一个区别是&#xff0c;调用一个子线程的 join() 方法后&#xff0c;该线程会一直被阻塞直到子线程执行完毕&#xff0c;而 CountDownLatch 可以在子线程运行的任何时候让 await 方法返回而不一定必须等到线程结束。另外&#xff0c;使用线程池来管理线程时一般都是直接添加 Runnable 到线程池&#xff0c;这时候就没有办法再调用线程的 join 方法了&#xff0c;就是说 countDownLatch 相比 join 方法让我们对线程同步有更灵活的控制。

CountDownLatch 中的方法介绍


  1. void await()&#xff1a; 当线程调用 CountDownLatch 对象的 await 方法后&#xff0c;当前线程会被阻塞&#xff0c;直到下面情况之一发生才会返回&#xff1a;当所有线程都调用了 CountDownLatch 对象的 countDown 方法后&#xff0c;也就是当计数器的值为 0 时&#xff1b;其他线程调用了当前线程的 interupt() 方法中断了当前线程&#xff0c;当前线程会抛出 InterruptedException 异常&#xff0c;然后返回。
  2. boolean await(long timeout, TimeUnit unit)&#xff1a;当线程调用了 CountDownLatch 对象的该方法后&#xff0c;当前线程会被阻塞&#xff0c;直到下面情况之一发生才会返回&#xff1a;当所有线程都调用了 CountDownLatch 对象的 countDown 方法后&#xff0c;也就是当计数器的值为 0 时&#xff1b;其他线程调用了当前线程的 interupt() 方法中断了当前线程&#xff0c;当前线程会抛出 InterruptedException 异常&#xff0c;然后返回。
  3. void countDown()&#xff1a;线程调用该方法后&#xff0c;计数器的值递减&#xff0c;递减后如果计数器值为 0 则唤醒所有因调用 await 方法而被阻塞的线程&#xff0c;否则什么都不做。
  4. long getCount()&#xff1a;获取当前计数器的值。

回环屏障 CyclicBarrier

上面介绍的 CountDownLatch 在解决多个线程同步方面相对于调用 join 方法已经有了不少优化&#xff0c;但是 CountDownLatch 的计数器是一次性的&#xff0c;也就是等到计数器变为 0 后&#xff0c;再调用 CountDownLatch 的 await 和 countdownLatch 方法都会立即返回&#xff0c;这就起不到线程同步的效果了。所以为了满足计数器可以重置的需要&#xff0c;JDK 开发组提供了 CyclicBarrier 类&#xff0c;并且 CyclicBarrier 类的功能并不限于 CountDownLatch 的功能。从字面意思理解&#xff0c;CyclicBarrier 是回环屏障的意思&#xff0c;它可以让一组线程全部达到一个状态后再全部同时执行。这里之所以叫做回环是因为当所有等待线程执行完毕&#xff0c;并重置 CyclicBarrier 的状态后它可以被重用。之所以叫做屏障是因为所有线程调用 await 方法后会被阻塞&#xff0c;这个阻塞点就称为屏蔽点&#xff0c;等所有线程都调用了 await 方法后&#xff0c;线程们就会冲破屏障&#xff0c;继续向下执行。

下面是一个演示&#xff1a;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;// 循环屏障
public class CyclicBarrierDemo1 {public static void main(String[] args) {// 循环屏障CyclicBarrier cyclicBarrier &#61; new CyclicBarrier(4, new Runnable() {&#64;Overridepublic void run() {System.out.println("计数器为 0 了");}});// 创建线程池ExecutorService service &#61; Executors.newFixedThreadPool(4);for (int i &#61; 0; i < 4; i&#43;&#43;) {int finalI &#61; i;service.submit(() -> {Thread currThread &#61; Thread.currentThread();System.out.println("执行线程&#xff1a;" &#43; currThread.getName());try {Thread.sleep(500 * finalI);cyclicBarrier.await(); // 执行阻塞等待&#xff08;直到循环屏障的计数器为0的时候&#xff0c;再执行后面的代码&#xff09;} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}System.out.println("线程执行完成:"&#43;currThread.getName());});}}
}

在这里插入图片描述

上面代码中&#xff0c;每个子线程在开始执行后都调用了 await 方法&#xff0c;等到所有线程都到达屏障点后才会一块往下执行&#xff0c;这就保证了所有线程的阶段性执行。

CyclicBarrier 中几个重要方法


  1. int await()&#xff1a;当线程调用 CyclicBarrier 的方法时会被阻塞&#xff0c;直到满足下面条件之一才会返回&#xff1a;parties 个线程都调用了 await() 方法&#xff0c;也就是线程都到了屏障点&#xff1b;其他线程调用了当前线程的 interrupt() 方法中断了当前线程&#xff0c;则当前线程会抛出 InterruptedException 异常而返回&#xff1b;与当前屏障点关联的 Generation 对象的 broken 标志被设置为 true 时&#xff0c;会抛出 BrokenBarrierException 异常&#xff0c;然后返回。
  2. boolean await(long timeout, TimeUnit unit)&#xff1a;当线程调用 CyclicBarrier 的方法时会被阻塞&#xff0c;直到满足下面条件之一才会返回&#xff1a;parties 个线程都调用了 await() 方法&#xff0c;也就是线程都到了屏障点&#xff0c;这时候返回true&#xff1b;设置的超时时间到了后返回 false&#xff1b;其他线程调用了当前线程的 interrupt() 方法中断了当前线程&#xff0c;则当前线程会抛出 InterruptedException 异常而返回&#xff1b;与当前屏障点关联的 Generation 对象的 broken 标志被设置为 true 时&#xff0c;会抛出 BrokenBarrierException 异常&#xff0c;然后返回。
  3. int dowait(boolean timed, long nanos)&#xff1a;该方法是 CyclicBarrier 的核心功能&#xff0c;当一个线程调用了 dowait 方法后&#xff0c;首先会获取独占锁 lock&#xff0c;如果创建 CyclicBarrier 时传递的参数为 10&#xff0c;那么后面 9 个调用线程会被阻塞。然后当前获取到的锁的线程会对计数器 count 进行递减操作&#xff0c;递减后 count &#61; index &#61; 9。如果当前线程调用了 await()&#xff0c;由于被阻塞释放锁后&#xff0c;其他被阻塞的 9 个线程中有一个会竞争到 lock 锁&#xff0c;然后执行同样的操作&#xff0c;直到最后一个线程获取到 lock 锁。最后 count &#61; index 等于 0&#xff0c;会重置 CyclicBarrier&#xff0c;然后这 10 个线程就可以继续向下运行了。

信号量 Semaphore

Semaphore 信号量也是 Java 中的一个同步器&#xff0c;与 CountDownLatch 和 CycleBarrier 不同的是&#xff0c;它内部的计数器是递增的&#xff0c;并且在一开始初始化 Semaphore 时可以指定一个初始值&#xff0c;但是并不需要知道同步的线程个数&#xff0c;而是在需要同步的地方调用 acquire 方法时指定需要同步的线程个数。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;public class SemaphoreDemo2 {private static Semaphore semaphore &#61; new Semaphore(0);public static void main(String[] args) throws InterruptedException {ExecutorService executorService &#61; Executors.newFixedThreadPool(2);for (int i &#61; 0; i < 2; i&#43;&#43;) {executorService.submit(new Runnable() {&#64;Overridepublic void run() {System.out.println(Thread.currentThread()&#43; "开始执行");semaphore.release();}});}// 等待子线程执行完毕&#xff0c;返回semaphore.acquire(2);System.out.println("所有线程执行完毕");// 关闭线程池executorService.shutdown();}
}

在这里插入图片描述
如上代码首先创建了一个信号量实例&#xff0c;构造函数的入参为 0&#xff0c;说明当前信号量计数器值为 0。然后 mian 函数向线程池添加两个线程任务&#xff0c;在每个线程内部调用信号量的 release 方法&#xff0c;这相当于让计数器值递增 1。最后在 main线程里面调用信号量的 acquire 方法&#xff0c;传参为 2 说明调用 acquire 方法的线程会一直阻塞&#xff0c;知道信号量的计数变为 2 才会返回。看到这里也就明白了&#xff0c;如果构造方法 Semaphore 时传递的参数为 N&#xff0c;并在 M 个线程中调用了该信号量的 release 方法&#xff0c;那么在调用 acquire 使 M 个线程同步时传递的参数应该是 M&#43;N。

Semaphore 主要方法


  1. void acquire()&#xff1a;当前线程调用该方法的目的是希望获取一个信号量资源。如果当前信号量个数大于0&#xff0c;则当前信号量的计数会减 1&#xff0c;然后该方法直接返回。否则如果当前信号量个数等于 0&#xff0c;则当前线程会被放入 AQS 的阻塞队列。当其他线程调用了当前线程的 interrupt() 方法中断了当前线程时&#xff0c;则会抛出 InterruptedException 异常返回。
  2. void acquire(int permits)&#xff1a; 该方法与 void acquire() 方法不同&#xff0c;后者只需要获取一个信号量值&#xff0c;而前者则获取 permits 个。
  3. void acquireUninterruptibly()&#xff1a;该方法与 void acquire() 类似&#xff0c;不同之处在于该方法对中断不响应&#xff0c;也就是当当前线程调用了 acquireUninterruptibly 获取资源是&#xff08;包含被阻塞后&#xff09;&#xff0c;其他线程调用了当前线程的 interrupt() 方法设置了当前线程的中断标志&#xff0c;此时当前线程并不会抛出 InterruptedException 异常而返回。
  4. void acquireUninterruptibly(int permits)&#xff1a;该方法与 void acquire(int permits) 方法不同之处在于&#xff0c;该方法对中断不响应。
  5. void release()&#xff1a;该方法的作用是把当前 Semaphore 对象的信号量值增加 1&#xff0c;如果当前有线程因为调用 aquire 方法被阻塞而被放入了 AQS 的阻塞队列&#xff0c;则会根据公平策略选择一个信号量个数能被满足的线程进行激活&#xff0c;激活的线程会尝试获取刚增加的信号量。
  6. void release(int permits)&#xff1a;该方法与不带参数的 release 方法的不同之处在于&#xff0c;前者每次调用会在信号量值原来的基础上增加 permits&#xff0c;而后者每次增加 1。

总结

此文介绍了并发包中关于线程协作的一些重要类。首先 CountDownLatch 通过计数器提供了更灵活的控制&#xff0c;只要检测到计数器值为 0&#xff0c;就可以往下执行&#xff0c;这相比于 join 必须等待线程执行完毕后主线程才会继续向下运行更灵活。另外&#xff0c;CyclicBarrier 也可以达到 CountDownLatch 的效果&#xff0c;但是后者在计数器值变为 0 后&#xff0c;就不能再被复用&#xff0c;而前者则可以使用 reset 方法重置后复用&#xff0c;前者对同一个算法但是输入参数不同的类似场景比较使用。而 Semaphore 采用了信号量递增的策略&#xff0c;一开始并不需要关心同步的线程个数&#xff0c;等调用 aquire 方法时再指定需要同步的个数&#xff0c;并且提供了获取信号量的公平性策略。


推荐阅读
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • 分享一款基于Java开发的经典贪吃蛇游戏实现
    本文介绍了一款使用Java语言开发的经典贪吃蛇游戏的实现。游戏主要由两个核心类组成:`GameFrame` 和 `GamePanel`。`GameFrame` 类负责设置游戏窗口的标题、关闭按钮以及是否允许调整窗口大小,并初始化数据模型以支持绘制操作。`GamePanel` 类则负责管理游戏中的蛇和苹果的逻辑与渲染,确保游戏的流畅运行和良好的用户体验。 ... [详细]
  • 本文深入探讨了Java多线程环境下的同步机制及其应用,重点介绍了`synchronized`关键字的使用方法和原理。`synchronized`关键字主要用于确保多个线程在访问共享资源时的互斥性和原子性。通过具体示例,如在一个类中使用`synchronized`修饰方法,展示了如何实现线程安全的代码块。此外,文章还讨论了`ReentrantLock`等其他同步工具的优缺点,并提供了实际应用场景中的最佳实践。 ... [详细]
  • 在Android应用开发中,实现与MySQL数据库的连接是一项重要的技术任务。本文详细介绍了Android连接MySQL数据库的操作流程和技术要点。首先,Android平台提供了SQLiteOpenHelper类作为数据库辅助工具,用于创建或打开数据库。开发者可以通过继承并扩展该类,实现对数据库的初始化和版本管理。此外,文章还探讨了使用第三方库如Retrofit或Volley进行网络请求,以及如何通过JSON格式交换数据,确保与MySQL服务器的高效通信。 ... [详细]
  • 本文介绍了UUID(通用唯一标识符)的概念及其在JavaScript中生成Java兼容UUID的代码实现与优化技巧。UUID是一个128位的唯一标识符,广泛应用于分布式系统中以确保唯一性。文章详细探讨了如何利用JavaScript生成符合Java标准的UUID,并提供了多种优化方法,以提高生成效率和兼容性。 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
  • 本文介绍了如何利用ObjectMapper实现JSON与JavaBean之间的高效转换。ObjectMapper是Jackson库的核心组件,能够便捷地将Java对象序列化为JSON格式,并支持从JSON、XML以及文件等多种数据源反序列化为Java对象。此外,还探讨了在实际应用中如何优化转换性能,以提升系统整体效率。 ... [详细]
  • 开发日志:201521044091 《Java编程基础》第11周学习心得与总结
    开发日志:201521044091 《Java编程基础》第11周学习心得与总结 ... [详细]
  • Java中不同类型的常量池(字符串常量池、Class常量池和运行时常量池)的对比与关联分析
    在研究Java虚拟机的过程中,笔者发现存在多种类型的常量池,包括字符串常量池、Class常量池和运行时常量池。通过查阅CSDN、博客园等相关资料,对这些常量池的特性、用途及其相互关系进行了详细探讨。本文将深入分析这三种常量池的差异与联系,帮助读者更好地理解Java虚拟机的内部机制。 ... [详细]
  • 深入解析 Android 中 EditText 的 getLayoutParams 方法及其代码应用实例 ... [详细]
  • 如何利用Java 5 Executor框架高效构建和管理线程池
    Java 5 引入了 Executor 框架,为开发人员提供了一种高效管理和构建线程池的方法。该框架通过将任务提交与任务执行分离,简化了多线程编程的复杂性。利用 Executor 框架,开发人员可以更灵活地控制线程的创建、分配和管理,从而提高服务器端应用的性能和响应能力。此外,该框架还提供了多种线程池实现,如固定线程池、缓存线程池和单线程池,以适应不同的应用场景和需求。 ... [详细]
  • 本文探讨了 Java 中 Pair 类的历史与现状。虽然 Java 标准库中没有内置的 Pair 类,但社区和第三方库提供了多种实现方式,如 Apache Commons 的 Pair 类和 JavaFX 的 javafx.util.Pair 类。这些实现为需要处理成对数据的开发者提供了便利。此外,文章还讨论了为何标准库未包含 Pair 类的原因,以及在现代 Java 开发中使用 Pair 类的最佳实践。 ... [详细]
  • Vue应用预渲染技术详解与实践 ... [详细]
  • AIX编程挑战赛:AIX正方形问题的算法解析与Java代码实现
    在昨晚的阅读中,我注意到了CSDN博主西部阿呆-小草屋发表的一篇文章《AIX程序设计大赛——AIX正方形问题》。该文详细阐述了AIX正方形问题的背景,并提供了一种基于Java语言的解决方案。本文将深入解析这一算法的核心思想,并展示具体的Java代码实现,旨在为参赛者和编程爱好者提供有价值的参考。 ... [详细]
author-avatar
wocanimagebi
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有