线程与锁
1. Java中线程的6种状态
-
**就绪(Runnable)和运行(Running)**的区别:
就绪是获得了运行资格、等待操作系统分配CPU时间片;运行则是真正占有CPU在执行指令。 -
等待(Waiting/Timed Waiting/Blocked) :线程等待外部条件满足才能继续执行,不只是等待CPU时间片,还可能是等资源、等锁等。
-
阻塞(Blocked) :线程被动等待同步锁(如synchronized)时,进入阻塞态。
- 注意:只有
synchronized
关键字可以让线程进入“阻塞态”(Blocked),仅调用lock(如ReentrantLock)不会进入Blocked,只可能进入等待或超时等待(Timed Waiting)。
- 注意:只有
-
阻塞与等待的区别:
- 阻塞是“被迫进入”,如等待锁释放;
- 等待是“主动进入”,如调用wait()。
2. 锁的释放时机
会释放锁的操作:
- 当前线程执行完同步方法或同步代码块;
- 当前线程在同步代码块、同步方法中遇到
break
或return
终止执行; - 当前线程在同步代码块、同步方法中抛出未捕获的
Error
或Exception
导致异常结束; - 当前线程在同步代码块、同步方法中调用了
wait()
方法,线程暂停并释放锁。
不会释放锁的操作:
- 线程在同步代码块或方法内调用
Thread.sleep()
或Thread.yield()
,只是暂停自己,不释放锁; - 线程在同步代码块时,其他线程对其调用
suspend()
方法将其挂起,该线程不会释放锁(同步监视器依然持有)。
3. 死锁的必要条件
- 互斥:资源同一时刻只能被一个线程/进程占用;
- 请求并保持:线程已保持了至少一个资源,同时请求新的资源;
- 不可剥夺:已获得的资源在使用前不能被剥夺;
- 循环等待:多个线程形成一种头尾相接的等待关系。
- 解决死锁的方法有:有序资源分配法、银行家算法等。
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 执行流程
- 多线程并发时,只有一个线程能成功将变量从期望值A修改为目标值B,其他线程CAS失败,会不断尝试(自旋),直到成功或放弃。
- 例如 AtomicInteger 的
incrementAndGet()
,如果多个线程同时执行 ++ 操作,只有一个能抢到修改权,其他线程检测到值被改动后会重新获取最新值再尝试。
举例说明:
假设有 5 个线程要把计数从 0 加到 5,每次 CAS 如果失败就重新尝试,直到目标值达成。
CAS 存在的问题
-
ABA 问题:
- 如果一个变量从A变成B,再从B变回A,CAS 并不知道值“变化过”,可能会导致错误的通过。
- 常用版本号/时间戳等机制解决,比如 Java 的
AtomicStampedReference
。
-
自旋开销:
- 如果高并发场景下竞争激烈,CAS 会不断自旋重试,浪费 CPU 资源。
-
一次只能操作一个变量:
- CAS 只能保证单个变量的原子性。如果需要多个变量整体原子性(比如复合操作),还是得用锁。
悲观锁与乐观锁
- 悲观锁:假设会发生冲突,所有操作都加锁(如
synchronized
)。 - 乐观锁:假设不会发生冲突,操作前不加锁,操作时如果发现数据被别人修改则重试(CAS 就是乐观锁的实现典型)。
性能对比:
小结
- CAS 是乐观锁的典型实现,适合无锁并发场景。
- 它能提升多核性能,但也存在 ABA 问题、自旋开销等局限性。
- 复杂同步场景还是需要配合锁机制使用。
阻塞队列与线程池
一、阻塞队列(BlockingQueue)
-
定义:阻塞队列是一种支持阻塞插入和移除操作的队列。用于解决生产者-消费者场景下的线程安全和资源平衡问题。
-
分类:
- 有界队列:容量有限,超出时生产者会阻塞(如
ArrayBlockingQueue
)。 - 无界队列:容量理论上无限,通常实现为链表(如
LinkedBlockingQueue
)。
- 有界队列:容量有限,超出时生产者会阻塞(如
-
常见实现:
- ArrayBlockingQueue:定长数组实现,有界。
- LinkedBlockingQueue:链表实现,可有界可无界(默认无界)。
- DelayQueue:带延时特性的无界队列,元素只有到期后才能被消费。
- SynchronousQueue:不存储元素,插入操作必须等待移除操作,适合任务直接交接。
- LinkedTransferQueue:支持生产者/消费者直接交接,transfer 方法可实现“先到先得”。
二、线程池
核心构造参数:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程最大空闲时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略)
线程池工作流程
- **核心线程(corePoolSize)**优先处理任务。
- 超出核心线程,任务进队列(BlockingQueue)。
- 队列满时,创建非核心线程(最大到 maximumPoolSize)。
- 再无可用线程,触发拒绝策略。
图示:1.核心线程,2.阻塞队列,3.非核心线程,4.拒绝策略
拒绝策略(四种):
- AbortPolicy:直接抛出异常(默认)。
- DiscardPolicy:直接丢弃新提交的任务。
- DiscardOldestPolicy:丢弃队列最老的任务,把新任务插进去。
- 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并复写其核心方法(如
tryAcquire
、tryRelease
),对外暴露 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在此基础上实现了公平锁/非公平锁,公平锁需检测排队顺序,非公平锁可以直接尝试抢占。
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 通过“主内存-工作内存”模型隔离线程间的并发冲突。
2. 并发下的数据安全问题
-
线程安全问题主要包括:可见性、原子性、有序性。
- 可见性:一个线程对共享变量的修改,其他线程能否看到。
- 原子性:一次操作要么全部成功,要么全部失败。
- 有序性:程序执行顺序符合代码顺序(JMM允许指令重排序)。
3. volatile 关键字
- volatile 是 JVM 提供的最轻量级同步机制,修饰的变量具备可见性和禁止指令重排序,但不保证原子性(如 i++ 就不是原子操作)。
作用
- 可见性:写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主流锁
1. CAS无锁编程的原理
-
CAS(Compare And Swap/Set)利用CPU原子指令实现无锁并发。
-
三大问题:
- ABA问题:变量在A→B→A之间变化,CAS无法感知,可用版本号解决。
- 自旋开销大:竞争激烈时反复尝试浪费CPU。
- 只能保证单变量原子性:多变量复合操作需借助锁或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参数可在所有线程到齐时统一执行合并等动作。
表格对比:
特点 | CountDownLatch | CyclicBarrier |
---|---|---|
可复用 | 否 | 是 |
适用场景 | 线程等待一组任务完成 | 多线程多阶段/多轮同步协作 |
唤醒方式 | 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 = “汇合 → 一起走” (可循环)。
参考文章:
Java 多线程和线程同步:juejin.cn/editor/draf…
学后检测
一、单选题(每题 2 分)
- 下列关于
volatile
关键字的说法正确的是:
A. 保证可见性和原子性
B. 只保证可见性和有序性,不保证原子性
C. 只保证原子性
D. 既不保证可见性也不保证原子性
【答案】B
【解析】
volatile
只保证多线程间变量的可见性和禁止指令重排序,但不能保证原子性。
- 以下哪一项不是线程进入 BLOCKED 状态的原因?
A. 等待获得 synchronized 锁
B. 调用 Object.wait()
C. 线程调用 sleep()
D. 线程调用 yield()
【答案】C
【解析】
sleep()
和yield()
都不会让线程进入 BLOCKED,只是暂停自己,BLOCKED 只因等待获取 synchronized 锁。
- 对于 Java 的 ReentrantLock 说法正确的是:
A. 只能作为非公平锁
B. 只支持独占锁
C. 支持可重入、公平/非公平、条件队列等特性
D. 是基于 synchronized 实现的
【答案】C
【解析】
ReentrantLock 是基于 AQS 实现的,功能远强于 synchronized,支持公平锁、条件队列等。
二、多选题(每题 3 分)
- 下列属于死锁发生的必要条件有( )
A. 互斥条件
B. 请求和保持
C. 可剥夺
D. 循环等待
E. 不可剥夺
【答案】A、B、D、E
【解析】
可剥夺是错误选项,必须是“不可以被剥夺”。四个必要条件为:互斥、请求与保持、不可剥夺、循环等待。
- 下列属于 Java 并发包(JUC)中基于 AQS 实现的同步器有( )
A. ReentrantLock
B. CountDownLatch
C. CyclicBarrier
D. Semaphore
【答案】A、B、D
【解析】
CyclicBarrier不是基于AQS,其他三个都是基于AQS的。
- 以下哪些场景适合使用 volatile 修饰变量?
A. 一个线程写,多个线程读
B. 变量只需保证可见性,无需复合原子操作
C. 实现双重检查锁单例
D. 计数器 i++ 的并发递增
【答案】A、B、C
【解析】
D 不适合用 volatile,因为 i++ 不是原子操作。
三、判断题(每题 2 分)
- synchronized 和 ReentrantLock 都是可重入锁。
【答案】√
【解析】
两者都支持同一线程重复获得锁,避免自锁死。
- Java 的 Lock 支持读写锁、可中断锁、超时锁等多种模式,而 synchronized 不支持。
【答案】√
【解析】
Lock 功能更丰富,synchronized 功能固定。
- 线程池的 submit 和 execute 方法都可以获取任务执行结果。
【答案】×
【解析】
只有 submit 可以返回 Future 获取结果,execute 没有返回值。
- 线程调用 wait() 时,会释放当前持有的锁。
【答案】√
【解析】
wait() 必须在同步块内执行,且会释放锁对象。
四、简答题(每题 5 分)
- 简述 synchronized 和 Lock 的区别。
【参考答案】
- synchronized 是 Java 内置关键字,基于 JVM 层的对象头和 Monitor 实现,代码简洁,但功能单一。
- Lock(如 ReentrantLock)是 Java API 实现,基于 AQS,支持公平/非公平、可重入、可中断、超时、条件队列等高级特性,使用上更灵活,但编码更复杂。
- synchronized 自动释放锁,Lock 需手动 unlock() 释放。
- 说明 CAS 的原理、优点和局限性。
【参考答案】
- CAS(Compare And Swap)利用 CPU 原子指令进行无锁并发,只要预期值等于内存值,就可安全更新为新值,否则重试。
- 优点:高效,无需加锁,减少阻塞,提高并发性能。
- 局限性:有 ABA 问题、自旋开销大、一次只能操作单变量(多变量需锁支持)。
- 如何合理配置线程池参数?
【参考答案】
- CPU密集型:线程数 ≈ CPU核心数 + 1,防止CPU因IO阻塞而空闲。
- IO密集型:线程数 ≈ CPU核心数 × 2 或更多,因为IO等待多于CPU计算。
- 具体参数建议根据业务场景做压测调整,避免内存溢出或CPU空转。
五、编程题(每题 8 分)
- 用 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()。
- 全部完成后主线程才会继续执行。
- 用 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("所有阶段完成,主线程继续");
}
}
解析
-
CyclicBarrier 构造
new CyclicBarrier(N, barrierAction)
:指定参与线程数为N
,到齐时自动执行barrierAction
。
-
分两轮循环
- 每个线程内部用
for (round)
模拟多轮任务; - 每轮
barrier.await()
会阻塞线程,直到 第 N 个await()
到来。
- 每个线程内部用
-
barrierAction
- 在所有线程到达屏障点时运行一次,打印“第 X 轮所有线程已就位”;
CyclicBarrier
自动重置,可供下一轮复用。
-
主线程等待
- 因
CyclicBarrier
不提供主线程 await,示例中让主线程睡足够时间; - 也可用额外的 CountDownLatch 或在最后一轮的 barrierAction 中通知主线程。
- 因
这样就能在两轮并发阶段间,确保所有子线程同步进入下一阶段。