Java8中JUC包同步工具类深度解析(Semaphore,CountDownLatch,CyclicBarrier,Phaser)

简介: Java8中JUC包同步工具类深度解析(Semaphore,CountDownLatch,CyclicBarrier,Phaser)

阿里非典型程序员一枚 ,记录平平无奇程序员在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名

引言

在Java中,并发编程一直是一个重要的领域,而JDK 8中的java.util.concurrent(JUC)包提供了丰富的同步工具类,如CountDownLatch、CyclicBarrier、Semaphore、Phaser,帮助开发者更加高效地处理并发问题,本文将逐一介绍。

一、Semaphore(信号量)

1. 简介

Semaphore是一种同步工具,它允许一定数量的线程同时访问共享资源。通过控制信号量的许可数量,Semaphore能够实现对共享资源的并发访问限制。

2. 适用场景

Semaphore适用于需要限制并发访问共享资源数量的场景。例如,数据库连接池中的连接数控制,防止过多的请求同时访问数据库;或者在分布式系统中限制某个服务能够处理的并发请求数,以保证服务的稳定性和响应速度。

3. 使用

Semaphore semaphore = new Semaphore(5); // 初始化信号量为5
semaphore.acquire(); // 获取一个许可,若信号量为0则阻塞
// 访问共享资源
semaphore.release(); // 释放一个许可

4. 内部原理及源码解读

内部原理

Semaphore基于AQS(AbstractQueuedSynchronizer)实现,它维护了一个许可计数器。当线程调用acquire()方法时,如果许可计数器大于0,则直接返回;否则线程会被加入等待队列并阻塞。当线程调用release()方法时,许可计数器加一,并尝试唤醒等待队列中的一个线程。

源码解读

Semaphore内部有一个类Sync,它继承了AbstractQueuedSynchronizer。Sync有两个子类:FairSync和NonfairSync,分别用于处理公平和非公平策略。

// Semaphore的构造方法
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

NonfairSyncFairSync中,会重写AQS的tryAcquiretryRelease等方法,来实现对许可计数器的增减操作以及线程的同步。

5. 注意事项

  • 使用Semaphore时,要确保释放的许可数量与获取的数量相匹配,避免造成死锁或资源泄漏。
  • 在高并发场景下,要合理设置信号量的初始值,以平衡资源利用率和并发性能。

二、CountDownLatch(倒计时锁)

1. 简介

CountDownLatch是一种同步工具,它允许一个或多个线程等待其他线程完成操作。通过维护一个计数器,当计数器减至0时,等待的线程将被唤醒。

2. 适用场景

CountDownLatch适用于需要等待一组线程完成某个任务后再继续执行的场景。例如,在启动多个线程进行并行计算时,可以使用CountDownLatch来等待所有线程计算完成后,主线程再进行汇总处理。

3. 使用

CountDownLatch latch = new CountDownLatch(5); // 初始化计数器为5
// ...其他线程执行操作,每完成一个操作调用latch.countDown()
latch.await(); // 当前线程等待,直到计数器减至0

4. 内部原理及源码解读

内部原理

CountDownLatch同样基于AQS实现,它维护了一个计数器。当线程调用countDown()方法时,计数器减一;当计数器减至0时,AQS会唤醒等待队列中的所有线程。

源码解读

CountDownLatch的核心在于AQS的state变量,它代表了计数器的值。

// CountDownLatch的构造方法
public CountDownLatch(int count) {
    // 初始化计数器
    sync = new Sync(count);
}
// Sync是CountDownLatch的内部类,继承了AQS
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 498226498192269037L;
    Sync(int count) {
        setState(count); // 设置AQS的state为初始计数器值
    }
    // ...其他方法,如tryAcquireShared等
}

tryAcquireShared方法中,会检查计数器的值是否为0,如果是则直接返回表示可以获取共享资源,否则将当前线程加入等待队列。当countDown方法被调用时,会调用releaseShared方法减少计数器的值,并尝试唤醒等待队列中的线程。

5. 注意事项

  • 在使用CountDownLatch时,要确保所有需要等待的线程都调用了countDown()方法,并且计数器的初始值设置正确。
  • 等待线程在调用await()方法后会被阻塞,直到计数器减至0,因此要避免在等待过程中执行耗时操作或阻塞操作。

三、CyclicBarrier(循环栅栏)

1. 简介

CyclicBarrier是一种同步工具,它允许一组线程互相等待,直到所有线程都到达某个公共屏障点(barrier point)。一旦所有线程都到达屏障点,它们可以继续执行后续操作。

2. 适用场景

CyclicBarrier适用于需要将一组线程分割成多个阶段,并在每个阶段完成后进行汇总或协调的场景。例如,在多个线程协同完成一个复杂任务时,每个线程负责不同的子任务,当所有线程都完成各自子任务后,再进行下一步操作。

3. 使用

CyclicBarrier cyclicBarrier = new CyclicBarrier(5); // 初始化栅栏,需要5个线程到达
// ...多个线程执行操作,到达屏障点时调用cyclicBarrier.await()
cyclicBarrier.await(); // 当前线程等待,直到所有线程都到达屏障点

4. 内部原理及源码解读

内部原理

CyclicBarrier内部使用了锁和条件变量来实现线程间的同步。当线程到达屏障点时,首先检查是否有足够的线程到达,如果有则继续执行;否则将线程加入等待队列并阻塞。当最后一个线程到达屏障点时,唤醒所有等待的线程。

源码解读

CyclicBarrier的核心在于其内部类Generation,它代表了屏障的某个周期。每个Generation都有一个计数器来记录到达屏障点的线程数量。

// CyclicBarrier的构造方法
public CyclicBarrier(int parties, Runnable barrierAction) {
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
    this.lock = new ReentrantLock();
    this.condition = lock.newCondition();
    this.generation = new Generation();
}
// Generation内部类
private static class Generation {
    boolean broken = false;
    int index = 0;
}

await方法中,线程会首先尝试获取锁,然后检查当前Generation的计数器是否为0。如果不为0,则线程会加入等待队列并阻塞。当最后一个线程到达屏障点时,它会修改Generation的计数器并唤醒等待队列中的所有线程。

5. 注意事项

  • 在使用CyclicBarrier时,要确保所有线程都正确调用了await()方法,并且屏障点的线程数量设置正确。
  • 如果在等待过程中发生异常或中断,CyclicBarrier可能会处于不一致状态,因此需要妥善处理异常和中断情况。

四、Phaser(阶段执行器)

1. 简介

Phaser是一种更灵活的同步工具,它提供了对一组线程进行分阶段同步的能力。Phaser允许线程注册、到达、等待和触发不同的阶段,非常适合用于需要动态管理线程阶段执行的场景。

2. 适用场景

Phaser适用于那些需要将线程划分为多个阶段,并在每个阶段结束时执行特定操作的情况。例如,在多阶段任务中,每个阶段可能需要不同的线程数量,且阶段的完成条件可能不同。使用Phaser,可以方便地对这些阶段进行管理和协调。

3. 使用

使用Phaser时,首先需要创建一个Phaser实例,并注册参与线程。然后,在每个阶段,线程可以调用arriveAndAwaitAdvance()方法来表示它们已经完成了当前阶段的工作,并等待其他线程完成。当所有线程都到达当前阶段时,Phaser会触发阶段转换,并允许线程进入下一个阶段。

4. 内部原理及源码解读

内部原理

Phaser内部维护了一个复杂的状态机,包括当前阶段数、已注册的参与者数量、已到达的参与者数量等。每个线程在Phaser中都有一个到达点,当所有线程都到达当前阶段时,Phaser会触发阶段转换,并允许线程进入下一个阶段。

源码解读

Phaser的源码相对复杂,它涉及到了大量的状态和计数器管理。其中,register方法用于注册参与者,arriveAndAwaitAdvance方法用于表示线程到达当前阶段并等待其他线程。在arriveAndAwaitAdvance方法中,会检查当前阶段是否已经完成,如果没有则增加已到达的参与者数量,并可能触发阶段转换。

深入理解Phaser的实现原理,查看和分析其源码是非常有帮助的。由于Phaser的源码较长且复杂,这里我聚焦于其核心机制,而不是完整的实现细节。

public class Phaser {
    // 表示参与者的数量,以及到达的参与者数量等状态信息
    private final AtomicLong state;
    // 用于等待/通知的锁
    private final Object lock;
    // 构造函数,初始化Phaser
    public Phaser() {
        state = new AtomicLong(Phaser.INITIAL_STATE);
        lock = new Object();
    }
    // 注册一个新的参与者,或者为已注册的参与者增加数量
    public void register() {
        // ... 省略具体的实现细节 ...
    }
    // 参与者到达某个阶段,并可能等待其他参与者
    public int arrive() throws InterruptedException {
        // ... 省略具体的实现细节 ...
        return phase;
    }
    // 参与者到达并等待其他参与者,同时推进到下一个阶段
    public int awaitAdvance(int phase) throws InterruptedException {
        // ... 省略具体的实现细节 ...
        return nextPhase;
    }
    // ... 其他方法,如deregister, arriveAndDeregister, bulkRegister, getPhase, getRegisteredParties等 ...
    // 内部状态表示,包含参与者数量和当前阶段等信息
    private static final long UNSET = -1L; // 用于表示未设置的值
    private static final long TERMINATED = Long.MAX_VALUE; // 表示Phaser已经终止
    private static final int MAX_PHASE = Integer.MAX_VALUE; // 最大阶段数
    private static final int PARTIES_MASK = 0xffff; // 参与者数量的掩码
    private static final int PHASE_MASK = ~PARTIES_MASK; // 阶段数的掩码
    private static final long INITIAL_STATE = (UNSET & PHASE_MASK) | (0 & PARTIES_MASK); // 初始状态
    // ... 其他内部方法和变量 ...
}

上面的代码只是一个框架,实际的Phaser实现要复杂得多。不过,通过这个框架,我们可以了解Phaser的一些核心组成部分:

  • 状态维护:Phaser使用一个AtomicLong类型的state变量来维护其内部状态。这个状态包含了当前阶段数、已注册的参与者数量以及已到达的参与者数量等信息。通过使用位操作和掩码,Phaser能够在单个原子变量中高效地存储和更新这些信息。
  • 注册与到达:register()方法用于注册新的参与者或增加已注册参与者的数量。arrive()方法用于表示参与者已经完成了当前阶段的工作,并可能等待其他参与者。这些方法会更新state变量中的相应信息,并根据需要唤醒等待的线程。
  • 等待与推进:awaitAdvance()方法用于等待其他参与者到达当前阶段,并一起进入下一个阶段。这个方法会根据state变量的状态来决定是否需要阻塞调用线程。当所有参与者都到达当前阶段时,Phaser会更新state变量以推进到下一个阶段,并唤醒所有等待的线程。
  • 中断与超时:实际的Phaser实现还支持响应中断和超时。这意味着如果线程在等待过程中被中断或超过指定的等待时间,它可以从等待状态中退出。这些特性是通过在内部使用锁和其他同步机制来实现的。

5. 注意事项

  • 在使用Phaser时,需要确保正确管理线程的注册和注销,避免在阶段转换时出现不一致的情况。
  • Phaser的灵活性也带来了一定的复杂性,因此在使用时需要深入理解其工作原理和使用方法,以避免出现错误或性能问题。

总结

横向对比

以下是以表格形式总结的JDK 8中JUC包中的Semaphore、CountDownLatch、CyclicBarrier和Phaser这四个同步工具类:

工具类 主要用途 内部原理 使用场景
Semaphore 控制访问某个或多个共享资源的线程数量 基于AQS实现,维护一个许可计数器 需要限制并发访问共享资源的场景,如连接池、线程池等
CountDownLatch 允许一个或多个线程等待其他线程完成操作 基于AQS实现,维护一个计数器 用于协调一组线程的执行顺序,例如启动多个线程并行处理任务,并在所有任务完成后执行汇总操作
CyclicBarrier 让一组线程互相等待,直到所有线程都到达某个公共屏障点 使用锁和条件变量实现,维护屏障的周期和计数器 需要一组线程在某个点相互等待的场景,如并行计算中的初始化、数据准备等
Phaser 提供对一组线程进行分阶段同步的能力 维护复杂的状态机,包括阶段数、参与者数量和到达点 适用于需要将线程划分为多个阶段,并在每个阶段结束时执行特定操作的场景,如多阶段任务处理

常见面试题

在面试中,关于JDK 8中JUC包中Semaphore、CountDownLatch、CyclicBarrier和Phaser这四个同步工具类的使用场景,可以提出以下面试题:

Semaphore使用场景面试题

  • 请描述一个你曾经使用Semaphore解决并发问题的场景。你是如何确定需要的许可数量的?
  • 在高并发环境下,如何使用Semaphore来限制对某个共享资源的访问数量?

CountDownLatch使用场景面试题

  • 假设你正在开发一个需要等待多个线程完成初始化操作的系统,你会如何使用CountDownLatch来实现?
  • 请分享一个你使用CountDownLatch协调多个线程执行顺序的实例,并解释其工作原理。

CyclicBarrier使用场景面试题

  • 描述一个适合使用CyclicBarrier的场景,并解释为什么它比其他同步工具类更适合这个场景。
  • 在一个多线程任务中,你需要在所有线程都完成某个阶段后才能进行下一阶段,你会如何使用CyclicBarrier来实现?

Phaser使用场景面试题

  • 请描述一个需要使用Phaser进行分阶段同步的场景,并解释Phaser在这个场景中的优势。
  • 假设你正在开发一个复杂的多阶段任务,每个阶段需要不同数量的线程来完成,你会如何使用Phaser来管理这些线程的执行?

这些面试题旨在了解候选人对这些同步工具类应用场景的理解以及实际应用经验。通过回答这些问题,候选人可以展示他们对并发编程和JUC工具类的熟悉程度,以及解决实际问题的能力。


这些工具类都提供了灵活的同步机制,可以帮助开发者更好地控制和管理并发程序的执行。根据具体的使用场景和需求,可以选择合适的工具类来实现线程同步和协调。

以上就是JDK 8中JUC包中Semaphore、CountDownLatch、CyclicBarrier和Phaser这四个同步工具类的详细介绍。每个类都有其独特的使用场景和内部原理,了解并正确使用这些工具类,可以大大提高并发编程的效率和稳定性。

相关文章
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
573 0
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
Java
【源码】【Java并发】【AQS】从ReentrantLock、Semaphore、CutDownLunch、CyclicBarrier看AQS源码
前言 主播觉得,AQS的原理,就是通过这2个队列的协助,实现核心功能,同步队列(CLH队列)和条件队列(Condition队列)。 同步队列(CLH队列) 作用:管理需要获...
298 18
【源码】【Java并发】【AQS】从ReentrantLock、Semaphore、CutDownLunch、CyclicBarrier看AQS源码
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
683 5
|
Java 数据库 开发者
详细介绍SpringBoot启动流程及配置类解析原理
通过对 Spring Boot 启动流程及配置类解析原理的深入分析,我们可以看到 Spring Boot 在启动时的灵活性和可扩展性。理解这些机制不仅有助于开发者更好地使用 Spring Boot 进行应用开发,还能够在面对问题时,迅速定位和解决问题。希望本文能为您在 Spring Boot 开发过程中提供有效的指导和帮助。
2491 12
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
559 1
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
654 5
|
机器学习/深度学习 人工智能 监控
鸿蒙赋能智慧物流:AI类目标签技术深度解析与实践
在数字化浪潮下,物流行业面临变革,传统模式的局限性凸显。AI技术为物流转型升级注入动力。本文聚焦HarmonyOS NEXT API 12及以上版本,探讨如何利用AI类目标签技术提升智慧物流效率、准确性和成本控制。通过高效数据处理、实时监控和动态调整,AI技术显著优于传统方式。鸿蒙系统的分布式软总线技术和隐私保护机制为智慧物流提供了坚实基础。从仓储管理到运输监控再到配送优化,AI类目标签技术助力物流全流程智能化,提高客户满意度并降低成本。开发者可借助深度学习框架和鸿蒙系统特性,开发创新应用,推动物流行业智能化升级。
530 1
|
人工智能 JSON Java
列表结构与树结构转换分析与工具类封装(java版)
本文介绍了将线性列表转换为树形结构的实现方法及工具类封装。核心思路是先获取所有根节点,将其余节点作为子节点,通过递归构建每个根节点的子节点。关键在于节点需包含 `id`、`parentId` 和 `children` 三个属性。文中提供了两种封装方式:一是基于基类 `BaseTree` 的通用工具类,二是使用函数式接口实现更灵活的方式。推荐使用后者,因其避免了继承限制,更具扩展性。代码示例中使用了 Jackson 库进行 JSON 格式化输出,便于结果展示。最后总结指出,理解原理是进一步优化和封装的基础。
512 0
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。

推荐镜像

更多
  • DNS