1. 为什么面试官总爱问同步工具类?
这个问题几乎出现在90%的中高级Java开发岗位面试中。我作为面试官时也经常用它来考察候选人对并发编程的理解深度。同步工具类就像多线程编程中的交通信号灯,控制着各个线程的通行顺序和协作方式。如果连这些基础工具都掌握不好,在实际开发中很容易出现死锁、数据竞争等严重问题。
去年我们团队就遇到过这样一个生产事故:某个订单处理系统在高并发场景下频繁出现订单状态不一致。排查后发现是开发人员错误使用了Semaphore,导致某些关键操作没有被正确同步。这个事故直接造成了数十万元的损失,也让我更加重视对同步工具类的考察。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四大同步工具类核心区别解析
2.1 CountDownLatch:一次性门闩机制
CountDownLatch是我在项目中用得最多的同步工具。它的核心思想就像一个倒计时门闩 - 初始化时设置计数,每次countDown()减1,await()会阻塞直到计数归零。
典型使用场景:
- 微服务启动时等待所有依赖服务就绪
- 批量任务执行完成后触发汇总操作
- 性能测试中同步所有压测线程的启动时刻
关键点:
- 计数不可重置,是一次性使用的
- 主线程和子线程都可以执行countDown()
- await()可以设置超时时间避免永久阻塞
java复制// 典型用法示例
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
// 任务1
latch.countDown();
}).start();
new Thread(() -> {
// 任务2
latch.countDown();
}).start();
new Thread(() -> {
// 任务3
latch.countDown();
}).start();
latch.await(); // 等待所有任务完成
System.out.println("所有任务已完成");
2.2 CyclicBarrier:可循环使用的栅栏
CyclicBarrier就像马拉松比赛中的集合点 - 所有线程到达栅栏后才会一起继续执行。与CountDownLatch最大的区别是它可以重置重复使用。
实际项目案例:
- 分阶段处理数据时,等待所有线程完成当前阶段
- 多线程测试中同步测试进度
- 迭代算法中同步各工作线程的迭代步骤
特别注意:
- 构造时可指定到达栅栏后的回调动作
- 如果线程被中断或超时,会抛出BrokenBarrierException
- reset()方法可以强制重置栅栏状态
java复制CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已到达栅栏");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 到达栅栏");
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 继续执行");
}).start();
}
2.3 Semaphore:流量控制的信号灯
Semaphore用于控制同时访问某资源的线程数量,就像高速路上的收费站 - 只有拿到通行证的车辆才能通过。
经典使用场景:
- 数据库连接池管理
- 限流保护后端服务
- 资源池的实现
重要特性:
- 支持公平/非公平模式
- 可以一次性获取/释放多个许可
- tryAcquire()支持非阻塞尝试
java复制// 实现一个简单的连接池
class ConnectionPool {
private final Semaphore semaphore;
private final LinkedList<Connection> pool = new LinkedList<>();
public ConnectionPool(int size) {
semaphore = new Semaphore(size);
for (int i = 0; i < size; i++) {
pool.addLast(createConnection());
}
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire();
synchronized (pool) {
return pool.removeFirst();
}
}
public void releaseConnection(Connection conn) {
synchronized (pool) {
pool.addLast(conn);
}
semaphore.release();
}
}
2.4 Phaser:更灵活的阶段同步器
Phaser是Java7引入的更强大的同步工具,可以看作CyclicBarrier的增强版,支持动态调整参与线程数。
适用场景:
- 分阶段的任务处理
- 动态增减参与线程的并行计算
- 复杂的多阶段工作流
优势特点:
- 支持分层结构降低竞争
- 可以动态注册/注销参与方
- 每个阶段都有唯一的phase number
java复制Phaser phaser = new Phaser(3); // 3个参与方
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 完成阶段1");
phaser.arriveAndAwaitAdvance();
System.out.println(Thread.currentThread().getName() + " 完成阶段2");
phaser.arriveAndAwaitAdvance();
System.out.println(Thread.currentThread().getName() + " 完成阶段3");
phaser.arriveAndDeregister();
}).start();
}
3. 线程顺序执行的8种实现方式
3.1 join()方法 - 最简单的顺序控制
java复制Thread t1 = new Thread(() -> System.out.println("线程1"));
Thread t2 = new Thread(() -> System.out.println("线程2"));
Thread t3 = new Thread(() -> System.out.println("线程3"));
t1.start();
t1.join(); // 等待t1结束
t2.start();
t2.join(); // 等待t2结束
t3.start();
注意:join()会完全阻塞当前线程,在复杂场景下可能导致性能问题
3.2 单线程池 - 天然的串行执行
java复制ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("任务1"));
executor.submit(() -> System.out.println("任务2"));
executor.submit(() -> System.out.println("任务3"));
executor.shutdown();
3.3 使用CountDownLatch实现顺序触发
java复制CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
new Thread(() -> {
System.out.println("线程1");
latch1.countDown();
}).start();
new Thread(() -> {
try {
latch1.await();
System.out.println("线程2");
latch2.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
latch2.await();
System.out.println("线程3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
3.4 使用阻塞队列传递控制权
java复制BlockingQueue<Boolean> queue = new ArrayBlockingQueue<>(1);
new Thread(() -> {
System.out.println("线程1");
queue.offer(true);
}).start();
new Thread(() -> {
try {
queue.take();
System.out.println("线程2");
queue.offer(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
queue.take();
System.out.println("线程3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
3.5 使用CompletableFuture链式调用
java复制CompletableFuture.runAsync(() -> System.out.println("任务1"))
.thenRun(() -> System.out.println("任务2"))
.thenRun(() -> System.out.println("任务3"))
.join();
3.6 使用Condition条件变量
java复制ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
AtomicInteger flag = new AtomicInteger(1);
new Thread(() -> {
lock.lock();
try {
while (flag.get() != 1) {
condition1.await();
}
System.out.println("线程1");
flag.set(2);
condition2.signal();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
lock.lock();
try {
while (flag.get() != 2) {
condition2.await();
}
System.out.println("线程2");
flag.set(3);
condition1.signal();
} finally {
lock.unlock();
}
}).start();
3.7 使用wait/notify机制
java复制Object lock = new Object();
AtomicInteger state = new AtomicInteger(1);
new Thread(() -> {
synchronized (lock) {
while (state.get() != 1) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1");
state.set(2);
lock.notifyAll();
}
}).start();
new Thread(() -> {
synchronized (lock) {
while (state.get() != 2) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程2");
state.set(3);
lock.notifyAll();
}
}).start();
3.8 使用LockSupport的park/unpark
java复制Thread t1 = new Thread(() -> {
System.out.println("线程1");
});
Thread t2 = new Thread(() -> {
LockSupport.park();
System.out.println("线程2");
});
Thread t3 = new Thread(() -> {
LockSupport.park();
System.out.println("线程3");
});
t3.start();
t2.start();
t1.start();
LockSupport.unpark(t2);
LockSupport.unpark(t3);
4. 面试中常见问题与回答技巧
4.1 "CountDownLatch和CyclicBarrier有什么区别?"
标准回答:
- 计数机制:CountDownLatch是递减不可重置,CyclicBarrier是递增可循环使用
- 使用场景:CountDownLatch用于一个线程等待多个线程,CyclicBarrier用于多个线程互相等待
- 异常处理:CyclicBarrier有BrokenBarrierException,CountDownLatch没有
- 回调功能:CyclicBarrier可以设置到达栅栏后的回调动作
加分回答:
"在我们最近的项目中,用CountDownLatch来等待所有微服务健康检查通过后才开放流量,而用CyclicBarrier来同步分布式计算的各个工作节点。"
4.2 "Semaphore有什么实际应用场景?"
标准回答:
- 资源池管理(如数据库连接池)
- 限流保护(如API调用限流)
- 生产者消费者问题
- 控制同时执行的线程数量
加分回答:
"我们系统使用Semaphore实现了动态限流,根据系统负载自动调整许可数量。当CPU使用率超过阈值时减少许可数,低于阈值时增加许可数,实现了自适应流量控制。"
4.3 "如何选择适合的线程顺序控制方式?"
标准回答需要考虑:
- 性能要求:join()最简单但性能最差
- 复杂度:CompletableFuture最简洁
- 灵活性:Condition和LockSupport最灵活
- 可维护性:单线程池最易理解
加分回答:
"在最近的一个订单处理系统中,我们使用CompletableFuture链式调用实现订单状态机的顺序流转,代码简洁且易于维护。而在另一个需要精确控制唤醒顺序的日志处理系统中,我们选择了Condition条件变量方式。"
5. 实际项目中的避坑经验
5.1 CountDownLatch的常见陷阱
- 计数不足导致永久阻塞
确保在所有执行路径上都调用了countDown()
- 计数过多导致提前唤醒
仔细计算需要的计数数量
- 重用问题
CountDownLatch是一次性的,需要重新创建
案例:我们曾遇到一个任务超时后没有调用countDown(),导致主线程永久阻塞。解决方法是在finally块中调用countDown()。
5.2 CyclicBarrier的性能问题
- 大量线程竞争栅栏会导致性能下降
考虑使用Phaser的分层结构
- 回调动作执行时间过长会阻塞所有线程
确保回调动作尽可能轻量
- 重置时机的选择
确保所有线程都离开后再调用reset()
5.3 Semaphore的正确释放
- 获取和释放不匹配
使用try-finally确保释放
java复制Semaphore semaphore = new Semaphore(1);
try {
semaphore.acquire();
// 临界区代码
} finally {
semaphore.release();
}
- 异常情况下的释放
考虑使用带超时的tryAcquire
- 公平性选择
非公平模式吞吐量更高,公平模式避免饥饿
5.4 线程顺序执行的注意事项
- 死锁风险
确保锁的获取和释放顺序一致
- 性能瓶颈
避免过度串行化影响吞吐量
- 调试困难
添加足够的日志记录执行顺序
在最近的一个项目中,我们使用线程局部变量来跟踪执行顺序,大大简化了调试过程。
