在高并发场景下,仅仅了解 synchronized 和 volatile 是远远不够的。面试时,面试官经常会考察你对 Java 并发工具类的掌握程度,比如 CountDownLatch、CyclicBarrier、Semaphore、Exchanger,以及 BlockingQueue 家族等。本文将通过生活案例和代码示例,带你彻底掌握这些工具类,轻松应对并发编程的挑战。
1. CountDownLatch:倒计时器
生活案例:
想象一下,公司年会,老板要等所有人都到齐了才能开始抽奖。CountDownLatch 就相当于年会主持人,初始化时设定一个计数器,每来一个人,计数器就减一,直到计数器变为零,主持人宣布抽奖开始。
底层原理:
CountDownLatch 内部维护一个计数器,通过 await() 方法阻塞线程,直到计数器变为零。通过 countDown() 方法递减计数器。计数器一旦变为零,就不能再重置。
代码示例:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int totalThreads = 3; // 模拟三个线程
CountDownLatch latch = new CountDownLatch(totalThreads);
ExecutorService executor = Executors.newFixedThreadPool(totalThreads);
for (int i = 0; i < totalThreads; i++) {
executor.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 线程开始执行");
Thread.sleep((long) (Math.random() * 3000)); // 模拟任务执行时间
System.out.println(Thread.currentThread().getName() + " 线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 线程执行完毕,计数器减一
}
});
}
latch.await(); // 主线程等待所有子线程执行完毕
System.out.println("所有线程执行完毕,主线程继续执行");
executor.shutdown();
}
}
实战避坑:
- 计数器只能减不能增:
CountDownLatch的计数器一旦变为零,就无法重置,因此要确保初始值设置正确。 - 防止死锁:如果某个线程在
countDown()之前发生异常,导致计数器无法递减到零,可能会导致其他线程永久阻塞在await()方法上。可以使用try-finally块确保countDown()总是被执行。
2. CyclicBarrier:循环栅栏
生活案例:
朋友们一起自驾游,约定好每到一个景点都集合拍照,所有人到齐了才能前往下一个景点。CyclicBarrier 就相当于这个集合点,每到达一个人,就等待其他人,直到所有人都到达,大家一起出发。
底层原理:
CyclicBarrier 允许多个线程互相等待,直到所有线程都到达一个屏障点。与 CountDownLatch 的区别在于,CyclicBarrier 的计数器可以重置,因此可以循环使用。可以理解为Nginx的反向代理服务器,接收到一定量的并发请求,转发给后面的服务器进行处理,处理完毕后返回结果,然后重置等待下一次的并发。
代码示例:
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CyclicBarrierExample {
public static void main(String[] args) {
int parties = 3; // 模拟三个线程
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
System.out.println("所有线程已到达屏障点,开始执行下一步操作");
});
ExecutorService executor = Executors.newFixedThreadPool(parties);
for (int i = 0; i < parties; i++) {
final int threadNum = i;
executor.execute(() -> {
try {
System.out.println("线程 " + threadNum + " 开始执行");
Thread.sleep((long) (Math.random() * 3000)); // 模拟任务执行时间
System.out.println("线程 " + threadNum + " 到达屏障点,等待其他线程");
barrier.await(); // 等待其他线程到达屏障点
System.out.println("线程 " + threadNum + " 继续执行");
} catch (Exception e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
实战避坑:
BrokenBarrierException:如果某个线程在等待过程中中断,会导致其他线程抛出BrokenBarrierException异常。需要捕获并处理该异常。- 避免死锁:确保所有参与者都能到达屏障点,否则会导致死锁。
- 栅栏动作(Barrier Action):可以在所有线程到达屏障点后执行一个栅栏动作,通常用于合并结果或进行下一步准备工作。
3. Semaphore:信号量
生活案例:
停车场有 5 个停车位,Semaphore 就相当于停车场管理员,初始化时设定 5 个许可证,每来一辆车,管理员就发一个许可证,当许可证用完时,后面的车只能等待。当车辆离开时,管理员收回许可证,其他车辆才能进入。
底层原理:
Semaphore 用于控制对有限资源的访问。内部维护一个计数器,表示可用资源的数量。通过 acquire() 方法获取许可证,如果许可证数量为零,则阻塞线程。通过 release() 方法释放许可证,增加可用资源的数量。 类似于 Nginx 的并发连接数的控制。
代码示例:
import java.util.concurrent.Semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SemaphoreExample {
public static void main(String[] args) {
int permits = 5; // 模拟 5 个许可证
Semaphore semaphore = new Semaphore(permits);
ExecutorService executor = Executors.newFixedThreadPool(10); // 模拟 10 个线程竞争资源
for (int i = 0; i < 10; i++) {
final int threadNum = i;
executor.execute(() -> {
try {
semaphore.acquire(); // 获取许可证
System.out.println("线程 " + threadNum + " 获取到许可证,开始执行");
Thread.sleep((long) (Math.random() * 3000)); // 模拟任务执行时间
System.out.println("线程 " + threadNum + " 执行完毕,释放许可证");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可证
}
});
}
executor.shutdown();
}
}
实战避坑:
- 忘记释放许可证:如果线程在获取许可证后发生异常,导致
release()方法没有被执行,可能会导致其他线程一直阻塞。可以使用try-finally块确保release()总是被执行。 - 重复释放许可证:如果线程重复释放许可证,可能会导致可用资源数量超过初始值,造成资源竞争问题。要确保
acquire()和release()方法成对出现。
4. Exchanger:数据交换器
生活案例:
两个快递员需要交换包裹,Exchanger 就相当于一个交换站,两个快递员分别将包裹送到交换站,然后互相交换包裹。
底层原理:
Exchanger 允许两个线程交换数据。每个线程调用 exchange() 方法时,如果另一个线程已经到达交换点,则交换数据,否则阻塞等待另一个线程到达。
代码示例:
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExchangerExample {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> {
try {
String data1 = "线程 1 的数据";
System.out.println("线程 1 准备交换数据:" + data1);
String data2 = exchanger.exchange(data1); // 交换数据
System.out.println("线程 1 交换后的数据:" + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.execute(() -> {
try {
String data2 = "线程 2 的数据";
System.out.println("线程 2 准备交换数据:" + data2);
String data1 = exchanger.exchange(data2); // 交换数据
System.out.println("线程 2 交换后的数据:" + data1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.shutdown();
}
}
实战避坑:
- 超时等待:
exchange()方法可以设置超时时间,避免线程永久阻塞。 - 数据类型一致:确保两个线程交换的数据类型一致,否则会导致类型转换异常。
5. BlockingQueue:阻塞队列
生活案例:
餐厅的厨房和前台,厨房生产食物,放入队列中,前台从队列中取出食物提供给顾客。如果队列为空,前台需要等待;如果队列已满,厨房需要等待。
底层原理:
BlockingQueue 提供阻塞的 put() 和 take() 方法。当队列满时,put() 方法会阻塞直到队列有空闲空间;当队列为空时,take() 方法会阻塞直到队列中有元素。BlockingQueue 家族有很多实现类,比如 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等。
代码示例:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3); // 容量为 3 的阻塞队列
// 生产者线程
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
String data = "数据 " + i;
queue.put(data); // 放入队列,如果队列已满则阻塞
System.out.println("生产者放入数据:" + data);
Thread.sleep((long) (Math.random() * 1000));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
String data = queue.take(); // 从队列取出数据,如果队列为空则阻塞
System.out.println("消费者取出数据:" + data);
Thread.sleep((long) (Math.random() * 1000));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
实战避坑:
- 队列容量:选择合适的队列容量,避免队列过小导致生产者阻塞,或队列过大浪费内存。
- 阻塞超时:可以使用带超时时间的
offer()和poll()方法,避免线程永久阻塞。 - 中断处理:正确处理
InterruptedException异常,避免线程无法正常退出。
掌握这些 Java 并发工具类,不仅能让你在面试中游刃有余,更能在实际工作中编写出高效、稳定的并发程序。记住,理解底层原理和实战经验才是关键,祝你在并发编程的道路上越走越远!记住,要合理的使用线程池,比如通过宝塔面板的监控,观测服务器的CPU,内存等指标,来评估线程池的大小。
冠军资讯
代码旅行家