并发基础与CAS基本原理

829 阅读21分钟

线程与锁

1. Java中线程的6种状态

image.png

  • **就绪(Runnable)运行(Running)**的区别:
    就绪是获得了运行资格、等待操作系统分配CPU时间片;运行则是真正占有CPU在执行指令。

  • 等待(Waiting/Timed Waiting/Blocked) :线程等待外部条件满足才能继续执行,不只是等待CPU时间片,还可能是等资源、等锁等。

  • 阻塞(Blocked) :线程被动等待同步锁(如synchronized)时,进入阻塞态。

    • 注意:只有synchronized关键字可以让线程进入“阻塞态”(Blocked),仅调用lock(如ReentrantLock)不会进入Blocked,只可能进入等待或超时等待(Timed Waiting)。
  • 阻塞与等待的区别

    • 阻塞是“被迫进入”,如等待锁释放;
    • 等待是“主动进入”,如调用wait()。

2. 锁的释放时机

会释放锁的操作:

  1. 当前线程执行完同步方法或同步代码块;
  2. 当前线程在同步代码块、同步方法中遇到breakreturn终止执行;
  3. 当前线程在同步代码块、同步方法中抛出未捕获的ErrorException导致异常结束;
  4. 当前线程在同步代码块、同步方法中调用了wait()方法,线程暂停并释放锁。

不会释放锁的操作:

  1. 线程在同步代码块或方法内调用Thread.sleep()Thread.yield(),只是暂停自己,不释放锁;
  2. 线程在同步代码块时,其他线程对其调用suspend()方法将其挂起,该线程不会释放锁(同步监视器依然持有)。

3. 死锁的必要条件

  1. 互斥:资源同一时刻只能被一个线程/进程占用;
  2. 请求并保持:线程已保持了至少一个资源,同时请求新的资源;
  3. 不可剥夺:已获得的资源在使用前不能被剥夺;
  4. 循环等待:多个线程形成一种头尾相接的等待关系。
  • 解决死锁的方法有:有序资源分配法、银行家算法等。

4. 活锁与死锁的区别

  • 死锁:线程互相等待,永远阻塞,不能继续执行。
  • 活锁:线程不断地尝试获取锁并释放锁,线程实际上在“活着”但没有做任何实质业务工作,导致程序进展不下去。例如两个线程都尝试获取两个锁,不停让步,最终谁也得不到全部锁。
  • 解决活锁的常见方式是为每次获取锁操作添加Thread.sleep(),让调度具有随机性,增加获取资源的概率。

活锁示例:

时刻线程A行为线程B行为状态
1获取lock1成功获取lock2成功各自获得第一个锁
2尝试获取lock2失败尝试获取lock1失败都检测到第二把锁被占用
3释放lock1释放lock2各自释放已有的锁
4休眠/等待片刻休眠/等待片刻等待后,重新尝试
5重复上述步骤重复上述步骤死循环,无实际进展
A: 尝试lock1 → 成功
A: 尝试lock2 → 失败(B已持有),A释放lock1

B: 尝试lock2 → 成功
B: 尝试lock1 → 失败(A已持有),B释放lock2

A、B再次尝试,如此反复……

只有当线程获取到锁的顺序正确(如A获取顺序为2,1)时,才能继续执行并避免活锁。

CAS 的基本原理

CAS(Compare And Swap/Set,比较并交换) 是一种硬件级别支持的原子操作。它主要用于多线程并发环境下对共享变量的安全修改,常用于实现无锁(Lock-Free)算法。

  • 原理:CAS 需要三个操作数——内存地址(V)、旧值(A)、新值(B)。当且仅当 V 处的值等于 A 时,才会将 V 处的值更新为 B,否则什么都不做。整个过程由 CPU 保证原子性。
  • 在 Java 中,AtomicXXX(如 AtomicInteger)底层就是通过 CAS 实现的。
  • synchronized 虽然也能保证原子性,但它属于阻塞锁机制,和 CAS 是两种思路。

CAS 执行流程

  1. 多线程并发时,只有一个线程能成功将变量从期望值A修改为目标值B,其他线程CAS失败,会不断尝试(自旋),直到成功或放弃。
  2. 例如 AtomicInteger 的 incrementAndGet(),如果多个线程同时执行 ++ 操作,只有一个能抢到修改权,其他线程检测到值被改动后会重新获取最新值再尝试。

举例说明:
假设有 5 个线程要把计数从 0 加到 5,每次 CAS 如果失败就重新尝试,直到目标值达成。


CAS 存在的问题

  1. ABA 问题

    • 如果一个变量从A变成B,再从B变回A,CAS 并不知道值“变化过”,可能会导致错误的通过。
    • 常用版本号/时间戳等机制解决,比如 Java 的 AtomicStampedReference
  2. 自旋开销

    • 如果高并发场景下竞争激烈,CAS 会不断自旋重试,浪费 CPU 资源。
  3. 一次只能操作一个变量

    • CAS 只能保证单个变量的原子性。如果需要多个变量整体原子性(比如复合操作),还是得用锁。

悲观锁与乐观锁

  • 悲观锁:假设会发生冲突,所有操作都加锁(如 synchronized)。
  • 乐观锁:假设不会发生冲突,操作前不加锁,操作时如果发现数据被别人修改则重试(CAS 就是乐观锁的实现典型)。

性能对比:

image.png


小结

  • CAS 是乐观锁的典型实现,适合无锁并发场景。
  • 它能提升多核性能,但也存在 ABA 问题、自旋开销等局限性。
  • 复杂同步场景还是需要配合锁机制使用。

阻塞队列与线程池

一、阻塞队列(BlockingQueue)

  • 定义:阻塞队列是一种支持阻塞插入和移除操作的队列。用于解决生产者-消费者场景下的线程安全和资源平衡问题。

  • 分类

    • 有界队列:容量有限,超出时生产者会阻塞(如 ArrayBlockingQueue)。
    • 无界队列:容量理论上无限,通常实现为链表(如 LinkedBlockingQueue)。
  • 常见实现

    • ArrayBlockingQueue:定长数组实现,有界。
    • LinkedBlockingQueue:链表实现,可有界可无界(默认无界)。
    • DelayQueue:带延时特性的无界队列,元素只有到期后才能被消费。
    • SynchronousQueue:不存储元素,插入操作必须等待移除操作,适合任务直接交接。
    • LinkedTransferQueue:支持生产者/消费者直接交接,transfer 方法可实现“先到先得”。

BlockingQueue结构


二、线程池

核心构造参数:

public ThreadPoolExecutor(int corePoolSize,        // 核心线程数
                          int maximumPoolSize,     // 最大线程数
                          long keepAliveTime,      // 非核心线程最大空闲时间
                          TimeUnit unit,           // 时间单位
                          BlockingQueue<Runnable> workQueue, // 工作队列
                          ThreadFactory threadFactory,       // 线程工厂
                          RejectedExecutionHandler handler   // 拒绝策略)

线程池工作流程

  1. **核心线程(corePoolSize)**优先处理任务。
  2. 超出核心线程,任务进队列(BlockingQueue)。
  3. 队列满时,创建非核心线程(最大到 maximumPoolSize)。
  4. 再无可用线程,触发拒绝策略。

线程池原理

图示:1.核心线程,2.阻塞队列,3.非核心线程,4.拒绝策略


拒绝策略(四种):

  1. AbortPolicy:直接抛出异常(默认)。
  2. DiscardPolicy:直接丢弃新提交的任务。
  3. DiscardOldestPolicy:丢弃队列最老的任务,把新任务插进去。
  4. CallerRunsPolicy:由提交任务的线程自己执行该任务。

线程池提交任务的两种方式

  • execute(Runnable):无返回值,适合只需要执行任务。
  • submit(Runnable/Callable):可返回 Future,可获结果或捕获异常。

线程池的关闭

  • shutdown():平滑关闭,已提交任务会继续执行,不再接收新任务。
  • shutdownNow():立即关闭,尝试中断所有线程,已提交未执行的任务将被丢弃。

注意:线程池中的线程中断是“协作式”,取决于线程自身如何处理中断信号。


线程池参数的合理配置

  • CPU密集型:线程数 = CPU核心数 + 1

  • IO密集型:线程数 = CPU核心数 × 2(或更多,理论值 = (CPU时间+IO等待时间)/CPU时间 × CPU数)

    • IO操作耗时远大于CPU操作,线程应尽量不被IO阻塞而导致CPU空闲。
    • 实际项目建议根据业务场景做压测、调整。
  • 混合型:建议分为两类线程池分别处理。


扩展知识

  • DMA与零拷贝:现代IO通常直接操作内存(DMA),减少CPU参与。零拷贝如 mmap/DirectMemory 可减少用户态和内核态的拷贝次数,提高性能。
  • 线程池与BlockingQueue配合:既保证了任务调度高效,又能有效限流。

AQS 与 JMM


一、AQS(AbstractQueuedSynchronizer)

1. 基本原理

  • AQS 是 Java 并发包(JUC)中各种同步器(如 ReentrantLock、Semaphore、CountDownLatch 等)的核心基础。
  • AQS 通过一个 volatile int state 变量维护同步状态,所有操作都围绕 state 进行(如加锁、解锁、计数等)。
  • 一般用法是:自定义同步工具类通过继承 AQS并复写其核心方法(如 tryAcquiretryRelease),对外暴露 lock/unlock 等接口。
示例(独占锁的 acquire/release):
@Override
protected boolean tryAcquire(int arg) {
    if (compareAndSetState(0, 1)) { // CAS原子设置
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

@Override
protected boolean tryRelease(int arg) {
    if (getState() == 0) throw new IllegalMonitorStateException();
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
}

2. AQS 的队列与原理(CLH 队列)

  • 核心思想:将获取不到锁的线程打包成一个个 Node(节点),用队列(CLH/FIFO队列)维护。
  • 自旋+挂起机制:每个节点不断检测前驱节点的状态,如果前驱释放了锁,自己就能尝试获得锁;否则就挂起等待。
  • AQS在此基础上实现了公平锁/非公平锁,公平锁需检测排队顺序,非公平锁可以直接尝试抢占。

CLH队列示意图


3. 可重入锁实现原理

  • 可重入锁允许同一线程重复获得锁,多次进入不会死锁。原理是记录“锁的获取次数”,每获得一次 state++,每释放一次 state--,直到为0才真正释放锁。
  • 非重入锁会导致同线程再次加锁时死锁。
示例代码
public boolean tryAcquire(int acquires) {
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    } else if (getExclusiveOwnerThread() == Thread.currentThread()) {
        setState(getState() + 1);
        return true;
    }
    return false;
}

二、JMM(Java Memory Model)

1. 基本概念

  • JMM 规定了 Java 线程间如何共享和可见内存数据。
  • 线程只能操作自己的本地工作内存,不能直接操作主内存或其他线程的内存。
  • JMM 通过“主内存-工作内存”模型隔离线程间的并发冲突。

JMM示意图


2. 并发下的数据安全问题

  • 线程安全问题主要包括:可见性、原子性、有序性。

    • 可见性:一个线程对共享变量的修改,其他线程能否看到。
    • 原子性:一次操作要么全部成功,要么全部失败。
    • 有序性:程序执行顺序符合代码顺序(JMM允许指令重排序)。

并发安全问题示意图


3. volatile 关键字

  • volatile 是 JVM 提供的最轻量级同步机制,修饰的变量具备可见性和禁止指令重排序,但不保证原子性(如 i++ 就不是原子操作)。
作用
  • 可见性:写volatile变量,所有线程都能立即看到新值。
  • 禁止指令重排序:保证volatile前后的代码不被重排。
  • 不是锁,不具备互斥性;适合单写多读场景。
典型用法
  • 状态标志、单例模式中的“双重检查锁定”等。

volatile底层原理


4. 其他说明

  • Volatile + CAS:很多并发包的原子类(如 AtomicInteger)用 volatile 配合 CAS 实现无锁并发。
  • volatile 适合一个线程写,多个线程读的场景

Synchronized锁的升级流程:偏向锁 → 轻量级锁 → 重量级锁

背景

  • 线程被 block 后挂起与恢复,会发生两次上下文切换(运行→阻塞、阻塞→运行),耗时约 3~5ms。
  • 为减少上下文切换带来的性能损耗,JVM 针对 synchronized 实现了多级锁优化。

一、偏向锁(Biased Locking)

  • 设计初衷:大量情况下,同一把锁总是被同一个线程获取——既然如此,就直接把这把锁“偏向”给该线程。
  • 实现方式:对象头记录当前偏向的线程ID,后续如果还是同一线程进来,不做任何同步或CAS,直接进入同步块,极致加速。
  • 竞争出现时:如果其他线程来竞争这把锁,偏向锁会被撤销,升级为轻量级锁。此过程涉及撤销原有的偏向标记,甚至出现“stop the world”暂停所有线程,做对象头和栈帧的修复。

二、轻量级锁(Lightweight Lock)

  • 设计初衷:如果两个线程“有冲突”,但加锁/解锁非常快,是否可以避免挂起/唤醒的代价?
  • 实现方式:线程通过CAS将对象头中的锁标记抢过来,如果失败则自旋重试(进入自旋锁状态)。
  • 自旋限制:自旋有最大次数/时间,超过就会放弃,升级为重量级锁。

三、适应性自旋锁(Adaptive Spin Lock)

  • 动态调整自旋次数:JVM会根据之前同类锁获取的成功经验,动态调整自旋时间,让大部分锁在合适时间内自旋成功,进一步减少上下文切换的发生。

四、重量级锁(Heavyweight Lock)

  • 最终手段:自旋还没拿到锁,线程会被挂起(进入OS层面的阻塞),只有等待持锁线程释放锁并唤醒自己。此时性能损耗最大。

五、锁升级流程示意图

锁升级流程


六、不同锁的对比

锁对比

  • Synchronized:基于对象头和JVM实现,锁粗粒度,升级流程丰富。
  • Lock(如ReentrantLock) :基于AQS(Java实现),粒度更细、功能更强,支持公平/非公平、可中断、超时、条件队列等特性。

七、Synchronized的实现原理

实现原理

  • 核心点:通过对象头(Mark Word)记录锁标志和状态,JVM通过monitorenter/monitorexit字节码实现加锁和释放。
  • 多级锁设计(偏向→轻量级→重量级)是Java HotSpot虚拟机的“锁优化”成果,是当前高性能并发应用的基石。

Java主流锁及并发相关问题精要

Java主流锁

image.png

1. CAS无锁编程的原理

  • CAS(Compare And Swap/Set)利用CPU原子指令实现无锁并发。

    • 三大问题:

      1. ABA问题:变量在A→B→A之间变化,CAS无法感知,可用版本号解决。
      2. 自旋开销大:竞争激烈时反复尝试浪费CPU。
      3. 只能保证单变量原子性:多变量复合操作需借助锁或AtomicReference等封装。

2. ReentrantLock的实现原理

  • 可重入锁,同一线程可重复获得锁,多次释放才真正释放。

  • 核心机制

    • 内部有一个state计数器,跟踪锁的重入次数。
    • 基于AQS(AbstractQueuedSynchronizer)实现,用CLH队列维护等待线程,独占/共享模式灵活支持。
    • 支持公平与非公平、可中断/可超时获取、Condition条件队列等高级功能。

3. synchronized的实现原理与优化

  • JVM内置锁关键字,在字节码中用monitorenter/monitorexit实现。

  • 锁优化

    • 偏向锁 → 轻量级锁(自旋)→ 重量级锁(阻塞)
    • 锁消除/锁粗化/逃逸分析:JIT编译器对无竞争/可合并的锁做优化
  • 和Lock的主要区别:

    • synchronized用法简单,JVM保障,语法级别
    • Lock(如ReentrantLock)功能更灵活,支持更多并发场景

4. volatile原理与DCL中的作用

  • volatile不能保证原子性,只保证可见性和有序性

  • 在DCL(双重检查锁单例)里防止指令重排序造成的“未完全初始化对象暴露”。

    • new操作步骤有可能被重排,volatile禁止重排保证安全发布。

5. sleep、wait、yield的区别

方法是否释放锁是否可中断用途
sleep暂停当前线程
yield礼让,让出CPU
wait等待/唤醒机制
  • wait()的线程 需被notify/notifyAll唤醒,且在同步块内执行,唤醒后需重新竞争锁。

6. 三个线程顺序执行的实现

  • 用join串联:

    • C线程run中先调用B.join(),B线程run中再调用A.join(),保证顺序:A→B→C。

7. 乐观锁与悲观锁

  • 悲观锁:预期冲突多,先加锁再操作(如synchronized)。
  • 乐观锁:预期冲突少,操作前不加锁,修改时用CAS/版本号等检测并重试。

8. AQS和synchronized关系

  • 无直接实现关系,都用于并发控制但技术路线完全不同。

    • synchronized:JVM级,基于对象头和Monitor
    • AQS:Java库级框架(如ReentrantLock/CountDownLatch/Semaphore),基于CLH队列和CAS

9. CountDownLatch 和 CyclicBarrier 的区别

  • CountDownLatch:一次性倒计时门栓,适合“等待一组线程结束”。

  • CyclicBarrier:可重用屏障,适合“多线程分阶段同步”(分批分轮同步执行)。

    • barrierAction参数可在所有线程到齐时统一执行合并等动作。

表格对比:

特点CountDownLatchCyclicBarrier
可复用
适用场景线程等待一组任务完成多线程多阶段/多轮同步协作
唤醒方式countDown减到0自动唤醒全部线程await到齐自动唤醒

简单对比如下:

// CountDownLatch——一次性等待
CountDownLatch latch = new CountDownLatch(3);
// A、B、C 线程 doWork(); latch.countDown();
// 主线程:latch.await(); // 等三个都 countDown() 完

// CyclicBarrier——可循环等待
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("一轮到齐"));
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        doPhase1(); barrier.await();
        // 当第三个线程也执行到 `barrier.await()` 时,控制台会先打印 barrierAction,
        // 那之后 3 条 “开始第二阶段” 才会几乎同时出现,表示它们“齐步”进入下一阶段。
        doPhase2(); barrier.await();
        // ……
    }).start();
}
  • 上面 latch 用完就不能再 await()
  • barrier 在两次 await() 之间会自动重置,可多次复用。

结论

  • CountDownLatch = “倒计时 → 门开” (一次性);
  • CyclicBarrier = “汇合 → 一起走” (可循环)。

测试源码github.com/xingchaozha…

参考文章:

Java 多线程和线程同步:juejin.cn/editor/draf…

学后检测

一、单选题(每题 2 分)

  1. 下列关于 volatile 关键字的说法正确的是:
    A. 保证可见性和原子性
    B. 只保证可见性和有序性,不保证原子性
    C. 只保证原子性
    D. 既不保证可见性也不保证原子性
    【答案】B
    【解析】
    volatile 只保证多线程间变量的可见性和禁止指令重排序,但不能保证原子性。

  1. 以下哪一项不是线程进入 BLOCKED 状态的原因?
    A. 等待获得 synchronized 锁
    B. 调用 Object.wait()
    C. 线程调用 sleep()
    D. 线程调用 yield()
    【答案】C
    【解析】
    sleep()yield() 都不会让线程进入 BLOCKED,只是暂停自己,BLOCKED 只因等待获取 synchronized 锁。

  1. 对于 Java 的 ReentrantLock 说法正确的是:
    A. 只能作为非公平锁
    B. 只支持独占锁
    C. 支持可重入、公平/非公平、条件队列等特性
    D. 是基于 synchronized 实现的
    【答案】C
    【解析】
    ReentrantLock 是基于 AQS 实现的,功能远强于 synchronized,支持公平锁、条件队列等。

二、多选题(每题 3 分)

  1. 下列属于死锁发生的必要条件有( )
    A. 互斥条件
    B. 请求和保持
    C. 可剥夺
    D. 循环等待
    E. 不可剥夺
    【答案】A、B、D、E
    【解析】
    可剥夺是错误选项,必须是“不可以被剥夺”。四个必要条件为:互斥、请求与保持、不可剥夺、循环等待。

  1. 下列属于 Java 并发包(JUC)中基于 AQS 实现的同步器有( )
    A. ReentrantLock
    B. CountDownLatch
    C. CyclicBarrier
    D. Semaphore
    【答案】A、B、D
    【解析】
    CyclicBarrier不是基于AQS,其他三个都是基于AQS的。

  1. 以下哪些场景适合使用 volatile 修饰变量?
    A. 一个线程写,多个线程读
    B. 变量只需保证可见性,无需复合原子操作
    C. 实现双重检查锁单例
    D. 计数器 i++ 的并发递增
    【答案】A、B、C
    【解析】
    D 不适合用 volatile,因为 i++ 不是原子操作。

三、判断题(每题 2 分)

  1. synchronized 和 ReentrantLock 都是可重入锁。
    【答案】√
    【解析】
    两者都支持同一线程重复获得锁,避免自锁死。

  1. Java 的 Lock 支持读写锁、可中断锁、超时锁等多种模式,而 synchronized 不支持。
    【答案】√
    【解析】
    Lock 功能更丰富,synchronized 功能固定。

  1. 线程池的 submit 和 execute 方法都可以获取任务执行结果。
    【答案】×
    【解析】
    只有 submit 可以返回 Future 获取结果,execute 没有返回值。

  1. 线程调用 wait() 时,会释放当前持有的锁。
    【答案】√
    【解析】
    wait() 必须在同步块内执行,且会释放锁对象。

四、简答题(每题 5 分)

  1. 简述 synchronized 和 Lock 的区别。
    【参考答案】
  • synchronized 是 Java 内置关键字,基于 JVM 层的对象头和 Monitor 实现,代码简洁,但功能单一。
  • Lock(如 ReentrantLock)是 Java API 实现,基于 AQS,支持公平/非公平、可重入、可中断、超时、条件队列等高级特性,使用上更灵活,但编码更复杂。
  • synchronized 自动释放锁,Lock 需手动 unlock() 释放。

  1. 说明 CAS 的原理、优点和局限性。
    【参考答案】
  • CAS(Compare And Swap)利用 CPU 原子指令进行无锁并发,只要预期值等于内存值,就可安全更新为新值,否则重试。
  • 优点:高效,无需加锁,减少阻塞,提高并发性能。
  • 局限性:有 ABA 问题、自旋开销大、一次只能操作单变量(多变量需锁支持)。

  1. 如何合理配置线程池参数?
    【参考答案】
  • CPU密集型:线程数 ≈ CPU核心数 + 1,防止CPU因IO阻塞而空闲。
  • IO密集型:线程数 ≈ CPU核心数 × 2 或更多,因为IO等待多于CPU计算。
  • 具体参数建议根据业务场景做压测调整,避免内存溢出或CPU空转。

五、编程题(每题 8 分)

  1. 用 CountDownLatch 实现:主线程等待 3 个子线程全部完成后再继续执行。
import java.util.concurrent.CountDownLatch;

public class LatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 1; i <= 3; i++) {
            final int id = i;
            new Thread(() -> {
                System.out.println("子线程 " + id + " 开始");
                try { Thread.sleep(100 * id); } catch (InterruptedException ignored) {}
                System.out.println("子线程 " + id + " 完成");
                latch.countDown();
            }).start();
        }
        System.out.println("主线程等待子线程完成...");
        latch.await();
        System.out.println("所有子线程已完成,主线程继续。");
    }
}

【解析】

  • 主线程调用 latch.await() 阻塞,等待 3 个子线程 countDown()。
  • 全部完成后主线程才会继续执行。

  1. 用 ThreadLocal 为每个线程维护自己的计数器,实现 5 个线程独立累加各自的计数。
public class ThreadLocalCounterDemo {
    static ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 3; j++) {
                    int c = counter.get() + 1;
                    counter.set(c);
                }
                System.out.println(Thread.currentThread().getName() + " 计数器=" + counter.get());
                // 建议用完移除
                counter.remove();
            }).start();
        }
    }
}

【解析】

  • ThreadLocal 保证每个线程有独立的副本,不会相互干扰。
  • 最后移除,避免线程池环境内存泄漏。

3. 使用 CyclicBarrier 实现分阶段并发。 要求:启动 5 个子线程,它们各自执行两轮“模拟工作”。

  • 每轮工作结束后,调用 barrier.await() 等待所有线程到达屏障;
  • 当 5 个线程都到达屏障时,自动执行一次 barrierAction(打印“第 X 轮所有线程已就位”);
  • 然后所有线程同时进入下一轮;
  • 两轮全部结束后,主线程打印“所有阶段完成”。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;

public class BarrierDemo {
    public static void main(String[] args) {
        final int N = 5;
        final int ROUNDS = 2;
        // 用于记录当前轮次
        AtomicInteger roundCounter = new AtomicInteger(1);

        // barrierAction:当所有线程到达屏障时执行
        Runnable barrierAction = () -> {
            int round = roundCounter.getAndIncrement();
            System.out.println(">>> 第 " + round + " 轮所有线程已就位");
        };

        CyclicBarrier barrier = new CyclicBarrier(N, barrierAction);

        // 启动 N 个工作线程
        for (int i = 1; i <= N; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    for (int round = 1; round <= ROUNDS; round++) {
                        // 模拟工作:每轮等待不同行为时长
                        Thread.sleep(300 + id * 100);
                        System.out.println("线程 " + id + " 完成第 " + round + " 轮工作");
                        // 等待其他线程
                        barrier.await();
                    }
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "T" + id).start();
        }

        // 主线程等待两轮全部结束
        // (CyclicBarrier 本身不提供 await;这里简单地让主线程 sleep 足够时间)
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) { }
        System.out.println("所有阶段完成,主线程继续");
    }
}

解析

  1. CyclicBarrier 构造

    • new CyclicBarrier(N, barrierAction):指定参与线程数为 N,到齐时自动执行 barrierAction
  2. 分两轮循环

    • 每个线程内部用 for (round) 模拟多轮任务;
    • 每轮 barrier.await() 会阻塞线程,直到 第 N 个 await() 到来。
  3. barrierAction

    • 在所有线程到达屏障点时运行一次,打印“第 X 轮所有线程已就位”;
    • CyclicBarrier 自动重置,可供下一轮复用。
  4. 主线程等待

    • CyclicBarrier 不提供主线程 await,示例中让主线程睡足够时间;
    • 也可用额外的 CountDownLatch 或在最后一轮的 barrierAction 中通知主线程。

这样就能在两轮并发阶段间,确保所有子线程同步进入下一阶段。