面试必问!同步工具类区别与线程顺序执行的 8 招
在多线程编程的世界里,如何优雅地控制线程间的协作与执行顺序,是每个Java开发者必须掌握的技能。
今天,我们来探讨一下Java并发编程中的三大同步工具类 —— CountDownLatch、CyclicBarrier、Semaphore,以及如何实现线程的顺序执行。
1. 并发三剑客:CountDownLatch、CyclicBarrier、Semaphore
1.1 CountDownLatch:一次性倒计时门闩
核心机制:CountDownLatch内部维护一个计数器,初始值由构造函数传入。线程调用countDown()方法递减计数器,调用await()方法的线程会阻塞直到计数器归零。
典型应用场景:
-
主线程等待多个子线程完成初始化工作 -
并行计算中等待所有计算任务完成 -
测试并发程序时模拟多线程同时执行
代码示例:
CountDownLatch latch = new CountDownLatch(3);for (int i = 0; i < 3; i++) {new Thread(() -> {// 执行任务latch.countDown();}).start();}latch.await(); // 主线程阻塞直到计数为0
1.2 CyclicBarrier:可循环使用的屏障
核心机制:CyclicBarrier允许一组线程相互等待,直到所有线程都到达某个公共屏障点,然后这些线程才能继续执行。
典型应用场景:
-
多线程计算数据,最后合并计算结果 -
游戏服务器中等待所有玩家准备就绪 -
批量数据处理的分阶段执行
代码示例:
CyclicBarrier barrier = new CyclicBarrier(3, () -> {System.out.println("所有线程准备完毕,继续执行...");});for (int i = 0; i < 3; i++) {new Thread(() -> {// 阶段性处理barrier.await(); // 等待所有线程到达// 下一阶段}).start();}
1.3 Semaphore:计数信号量限流器
核心机制:Semaphore用于控制同时访问特定资源的线程数量,通过维护一组许可证来实现限制。
典型应用场景:
-
数据库连接池管理 -
限流控制 -
资源池实现
代码示例:
Semaphore semaphore = new Semaphore(2); // 最多2个线程同时访问for (int i = 0; i < 5; i++) {new Thread(() -> {try {semaphore.acquire();// 临界区逻辑} finally {semaphore.release();}}).start();}
1.4 三者的核心区别对比
|
|
|
|
|
|---|---|---|---|
| 可重用性 |
|
|
|
| 计数方向 |
|
|
|
| 主要用途 |
|
|
|
| 触发条件 |
|
|
|
| 线程角色 |
|
|
|
| 屏障动作 |
|
|
|
关键区别解析:
-
一次性 vs 循环性:CountDownLatch是一次性的,计数器归零后即失效;CyclicBarrier可循环使用,自动重置计数器;Semaphore持续管理许可证,无使用次数限制 -
等待模式:CountDownLatch是单向等待(主线程等子线程);CyclicBarrier是多向等待(所有线程相互等待);Semaphore是资源竞争(线程间无直接协调)
2. 线程顺序执行的八个方案
2.1 方案1:Thread.join()方法
核心原理:join()方法让当前线程阻塞,直到被调用线程执行完毕。本质是调用wait()方法实现等待。
适用场景:简单线程依赖,明确知道谁等待谁。
详细代码:
public class ThreadJoinDemo {publicstaticvoidmain(String[] args) {// 创建三个线程Thread t1 = new Thread(() -> {try {Thread.sleep(100); // 模拟耗时操作System.out.println(Thread.currentThread().getName() + "执行完成");} catch (InterruptedException e) {e.printStackTrace();}}, "T1");Thread t2 = new Thread(() -> {try {t1.join(); // T2等待T1执行完Thread.sleep(50);System.out.println(Thread.currentThread().getName() + "执行完成");} catch (InterruptedException e) {e.printStackTrace();}}, "T2");Thread t3 = new Thread(() -> {try {t2.join(); // T3等待T2执行完Thread.sleep(30);System.out.println(Thread.currentThread().getName() + "执行完成");} catch (InterruptedException e) {e.printStackTrace();}}, "T3");// 启动线程(注意:启动顺序可以任意,但执行顺序是T1→T2→T3)t1.start();t2.start();t3.start();// 主线程等待所有子线程完成try {t1.join();t2.join();t3.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("所有线程执行完毕");}}
执行结果:
T1执行完成T2执行完成T3执行完成所有线程执行完毕
注意事项:
-
join()可能会抛出InterruptedException,必须处理 -
可以指定超时时间: join(long millis) -
启动顺序不影响执行顺序, join()决定了谁等谁
2.2 方案2:CountDownLatch实现
核心原理:通过倒计时计数器实现线程等待,countDown()减1,await()阻塞直到计数器为0。
适用场景:一个线程需要等待多个线程完成的场景。
详细代码:
import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {// 创建两个倒计时门闩CountDownLatch latch1 = new CountDownLatch(1); // T1完成后计数-1CountDownLatch latch2 = new CountDownLatch(1); // T2完成后计数-1// 线程T1new Thread(() -> {try {Thread.sleep(100);System.out.println(Thread.currentThread().getName() + "执行完成");} catch (InterruptedException e) {e.printStackTrace();} finally {latch1.countDown(); // T1完成,计数器减1}}, "T1").start();// 线程T2new Thread(() -> {try {latch1.await(); // 等待T1完成Thread.sleep(50);System.out.println(Thread.currentThread().getName() + "执行完成");} catch (InterruptedException e) {e.printStackTrace();} finally {latch2.countDown(); // T2完成,计数器减1}}, "T2").start();// 线程T3new Thread(() -> {try {latch2.await(); // 等待T2完成Thread.sleep(30);System.out.println(Thread.currentThread().getName() + "执行完成");} catch (InterruptedException e) {e.printStackTrace();}}, "T3").start();// 等待T3完成(可选)TimeUnit.MILLISECONDS.sleep(200);System.out.println("所有线程执行完毕");}}
执行结果:
T1执行完成T2执行完成T3执行完成所有线程执行完毕
2.3 方案3:单线程线程池
核心原理:newSingleThreadExecutor()创建只有一个工作线程的线程池,任务在队列中按提交顺序执行。
适用场景:需要顺序执行但希望有队列管理的场景。
详细代码:
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;public class SingleThreadPoolDemo {public static void main(String[] args) {// 创建单线程线程池ExecutorService executor = Executors.newSingleThreadExecutor();// 提交任务(会按提交顺序执行)executor.submit(() -> {try {Thread.sleep(100);System.out.println("T1执行完成");} catch (InterruptedException e) {e.printStackTrace();}});executor.submit(() -> {try {Thread.sleep(50);System.out.println("T2执行完成");} catch (InterruptedException e) {e.printStackTrace();}});executor.submit(() -> {try {Thread.sleep(30);System.out.println("T3执行完成");} catch (InterruptedException e) {e.printStackTrace();}});// 关闭线程池executor.shutdown();try {// 等待所有任务完成if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {executor.shutdownNow();}} catch (InterruptedException e) {executor.shutdownNow();}System.out.println("所有线程执行完毕");}}
执行结果:
T1执行完成T2执行完成T3执行完成所有线程执行完毕
2.4 方案4:CompletableFuture链式调用
核心原理:Java 8提供的异步编程工具,thenRun()在前一个任务完成后执行下一个。
适用场景:异步任务编排,需要复杂依赖关系的场景。
详细代码:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 方法1:链式调用(推荐)CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {try {Thread.sleep(100);System.out.println("T1执行完成");} catch (InterruptedException e) {e.printStackTrace();}}).thenRun(() -> {try {Thread.sleep(50);System.out.println("T2执行完成");} catch (InterruptedException e) {e.printStackTrace();}}).thenRun(() -> {try {Thread.sleep(30);System.out.println("T3执行完成");} catch (InterruptedException e) {e.printStackTrace();}});// 等待所有任务完成future.get();System.out.println("所有线程执行完毕");// 方法2:分别创建(更灵活)System.out.println("\n--- 方法2:分别创建CompletableFuture ---");CompletableFuture<Void> f1 = CompletableFuture.runAsync(() ->System.out.println("任务1执行"));CompletableFuture<Void> f2 = f1.thenRun(() ->System.out.println("任务2执行"));CompletableFuture<Void> f3 = f2.thenRun(() ->System.out.println("任务3执行"));f3.get();}}
执行结果:
T1执行完成T2执行完成T3执行完成所有线程执行完毕--- 方法2:分别创建CompletableFuture ---任务1执行任务2执行任务3执行
2.5 方案5:CyclicBarrier实现
核心原理:让一组线程互相等待,到达屏障点后一起继续执行。通过创建多个屏障实现顺序。
详细代码:
import java.util.concurrent.BrokenBarrierException;import java.util.concurrent.CyclicBarrier;public class CyclicBarrierDemo {publicstaticvoidmain(String[] args) {// 创建两个屏障CyclicBarrier barrier1 = new CyclicBarrier(2); // T1和主线程CyclicBarrier barrier2 = new CyclicBarrier(2); // T2和主线程// 线程T1new Thread(() -> {try {Thread.sleep(100);System.out.println("T1执行完成");barrier1.await(); // 通知主线程T1已完成} catch (Exception e) {e.printStackTrace();}}).start();// 主线程等待T1完成try {barrier1.await();// 线程T2new Thread(() -> {try {Thread.sleep(50);System.out.println("T2执行完成");barrier2.await(); // 通知主线程T2已完成} catch (Exception e) {e.printStackTrace();}}).start();// 主线程等待T2完成barrier2.await();// 线程T3new Thread(() -> {try {Thread.sleep(30);System.out.println("T3执行完成");} catch (Exception e) {e.printStackTrace();}}).start();} catch (Exception e) {e.printStackTrace();}}}
2.6 方案6:Semaphore实现
核心原理:通过信号量控制线程执行权限,初始化许可数为0,前一个线程释放许可,后一个线程才能获取。
详细代码:
import java.util.concurrent.Semaphore;public class SemaphoreDemo {public static void main(String[] args) {// 创建两个信号量,初始许可数为0Semaphore semaphore1 = new Semaphore(0);Semaphore semaphore2 = new Semaphore(0);// 线程T1new Thread(() -> {try {Thread.sleep(100);System.out.println("T1执行完成");semaphore1.release(); // 释放一个许可} catch (InterruptedException e) {e.printStackTrace();}}).start();// 线程T2new Thread(() -> {try {semaphore1.acquire(); // 获取许可(等待T1完成)Thread.sleep(50);System.out.println("T2执行完成");semaphore2.release(); // 释放一个许可} catch (InterruptedException e) {e.printStackTrace();}}).start();// 线程T3new Thread(() -> {try {semaphore2.acquire(); // 获取许可(等待T2完成)Thread.sleep(30);System.out.println("T3执行完成");} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
2.7 方案7:ReentrantLock + Condition
核心原理:通过Condition的await()和signal()实现线程间的精准唤醒。
详细代码:
import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockDemo {private static ReentrantLock lock = new ReentrantLock();private static Condition condition1 = lock.newCondition();private static Condition condition2 = lock.newCondition();private static volatile int flag = 1; // 标志位publicstaticvoidmain(String[] args) {// 线程T1new Thread(() -> {lock.lock();try {while (flag != 1) {condition1.await();}Thread.sleep(100);System.out.println("T1执行完成");flag = 2;condition2.signal(); // 唤醒T2} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}).start();// 线程T2new Thread(() -> {lock.lock();try {while (flag != 2) {condition2.await();}Thread.sleep(50);System.out.println("T2执行完成");flag = 3;condition1.signal(); // 可以唤醒T1,但这里用不上} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}).start();// 线程T3new Thread(() -> {lock.lock();try {// 简单实现:等待一小段时间Thread.sleep(150);System.out.println("T3执行完成");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}).start();}}
2.8 方案8:BlockingQueue信号传递
核心原理:利用阻塞队列的take()和put()方法实现线程同步。
详细代码:
import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;public class BlockingQueueDemo {public static void main(String[] args) throws InterruptedException {// 创建两个容量为1的阻塞队列BlockingQueue<String> queue1 = new ArrayBlockingQueue<>(1);BlockingQueue<String> queue2 = new ArrayBlockingQueue<>(1);// 线程T1new Thread(() -> {try {Thread.sleep(100);System.out.println("T1执行完成");queue1.put("T1完成"); // 放入信号} catch (InterruptedException e) {e.printStackTrace();}}).start();// 线程T2new Thread(() -> {try {queue1.take(); // 等待T1的信号Thread.sleep(50);System.out.println("T2执行完成");queue2.put("T2完成"); // 放入信号} catch (InterruptedException e) {e.printStackTrace();}}).start();// 线程T3new Thread(() -> {try {queue2.take(); // 等待T2的信号Thread.sleep(30);System.out.println("T3执行完成");} catch (InterruptedException e) {e.printStackTrace();}}).start();// 等待执行完成Thread.sleep(200);System.out.println("所有线程执行完毕");}}
2.9 方案对比总结
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.10 如何选择?
-
简单场景用join:少量线程的简单依赖 -
等待多个用Latch:主线程需要等待多个子线程完成 -
任务队列用单线程池:需要任务排队执行 -
异步编排用Future:需要复杂异步任务依赖 -
分阶段用Barrier:多轮同步执行 -
限流用Semaphore:控制并发访问数量 -
精细控制用Lock:需要精准唤醒特定线程 -
解耦用Queue:生产者和消费者模式
3. 实战选型建议与最佳实践
3.1 如何选择合适的并发工具类?
选择CountDownLatch的场景:
-
需要主线程等待多个子任务完成再继续 -
一次性事件等待,如服务启动等待依赖初始化完成 -
测试环境中,主线程等待多个异步任务完成后再生成测试报告
选择CyclicBarrier的场景:
-
多线程分阶段处理任务,每阶段需要所有线程同步 -
需要循环使用的同步点,如迭代算法、模拟步进 -
多轮批处理任务,每轮都需要所有线程到达屏障点
选择Semaphore的场景:
-
需要控制同时访问资源的线程数量 -
限流场景,如数据库连接池管理 -
资源池实现,如连接池、对象池
3.2 常见陷阱
-
CountDownLatch的倒计时陷阱:调用
await()方法的线程会阻塞至计数器为0,但如果计数器未正确减至0(如线程异常终止),会导致永久阻塞。解决方案:使用await(long timeout, TimeUnit unit)设置超时时间 -
Semaphore的信号量管理:
acquire()和release()必须成对出现,否则会导致信号量失衡
4. 总结
Java并发编程的核心在于理解不同工具类的适用场景和生命周期限制。
CountDownLatch适合一次性事件等待,CyclicBarrier适合周期性同步,Semaphore适合资源访问控制。
对于线程顺序执行,根据场景复杂度可选择Thread.join()、CountDownLatch、单线程线程池或CompletableFuture等方案。
并发工具不是越多越好,而是要用对地方。在实际开发中,应权衡顺序执行的必要性,多数情况下线程池的并发特性才是其价值所在。掌握这些工具的正确使用方法,能让你的多线程程序更加健壮、高效。
夜雨聆风