乐于分享
好东西不私藏

面试必问!同步工具类区别与线程顺序执行的 8 招

面试必问!同步工具类区别与线程顺序执行的 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 三者的核心区别对比

特性
CountDownLatch
CyclicBarrier
Semaphore
可重用性
不可重用
可重用
可重用
计数方向
递减
递增
可增可减
主要用途
等待事件
线程同步
资源控制
触发条件
计数器减到0
等待线程数达到预设值
有可用许可
线程角色
主线程(等待) vs 工作线程(做事)
所有线程角色对等
线程角色无特定关系
屏障动作
不支持
支持(可选Runnable)
不支持

关键区别解析

  • 一次性 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完成后计数-1        CountDownLatch latch2 = new CountDownLatch(1);  // T2完成后计数-1        // 线程T1        new Thread(() -> {            try {                Thread.sleep(100);                System.out.println(Thread.currentThread().getName() + "执行完成");            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                latch1.countDown(); // T1完成,计数器减1            }        }, "T1").start();        // 线程T2        new 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();        // 线程T3        new 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和主线程        // 线程T1        new Thread(() -> {            try {                Thread.sleep(100);                System.out.println("T1执行完成");                barrier1.await(); // 通知主线程T1已完成            } catch (Exception e) {                e.printStackTrace();            }        }).start();        // 主线程等待T1完成        try {            barrier1.await();            // 线程T2            new Thread(() -> {                try {                    Thread.sleep(50);                    System.out.println("T2执行完成");                    barrier2.await(); // 通知主线程T2已完成                } catch (Exception e) {                    e.printStackTrace();                }            }).start();            // 主线程等待T2完成            barrier2.await();            // 线程T3            new 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) {        // 创建两个信号量,初始许可数为0        Semaphore semaphore1 = new Semaphore(0);        Semaphore semaphore2 = new Semaphore(0);        // 线程T1        new Thread(() -> {            try {                Thread.sleep(100);                System.out.println("T1执行完成");                semaphore1.release(); // 释放一个许可            } catch (InterruptedException e) {                e.printStackTrace();            }        }).start();        // 线程T2        new Thread(() -> {            try {                semaphore1.acquire(); // 获取许可(等待T1完成)                Thread.sleep(50);                System.out.println("T2执行完成");                semaphore2.release(); // 释放一个许可            } catch (InterruptedException e) {                e.printStackTrace();            }        }).start();        // 线程T3        new 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) {        // 线程T1        new 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();        // 线程T2        new 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();        // 线程T3        new 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);        // 线程T1        new Thread(() -> {            try {                Thread.sleep(100);                System.out.println("T1执行完成");                queue1.put("T1完成"); // 放入信号            } catch (InterruptedException e) {                e.printStackTrace();            }        }).start();        // 线程T2        new Thread(() -> {            try {                queue1.take(); // 等待T1的信号                Thread.sleep(50);                System.out.println("T2执行完成");                queue2.put("T2完成"); // 放入信号            } catch (InterruptedException e) {                e.printStackTrace();            }        }).start();        // 线程T3        new 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 方案对比总结

方案
核心类
优点
缺点
适用场景
1.join()
Thread
简单直观
硬编码依赖关系
简单线程控制
2.CountDownLatch
CountDownLatch
灵活,可复用
一次性使用
主线程等待多个子线程
3.单线程池
ExecutorService
自动管理线程
失去并发优势
需要队列管理的任务
4.CompletableFuture
CompletableFuture
异步编排强大
学习成本高
复杂异步任务流
5.CyclicBarrier
CyclicBarrier
可循环使用
配置稍复杂
分阶段同步
6.Semaphore
Semaphore
控制并发数
信号量管理复杂
限流、资源控制
7.ReentrantLock
ReentrantLock+Condition
精准控制
代码复杂
需要精细控制
8.BlockingQueue
BlockingQueue
解耦生产消费
需要队列管理
生产者消费者模式

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等方案。

并发工具不是越多越好,而是要用对地方。在实际开发中,应权衡顺序执行的必要性,多数情况下线程池的并发特性才是其价值所在。掌握这些工具的正确使用方法,能让你的多线程程序更加健壮、高效。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 面试必问!同步工具类区别与线程顺序执行的 8 招

猜你喜欢

  • 暂无文章