在 Java 并发编程中,java.util.concurrent 包下的并发工具类是面试的常客,也是实际开发中解决高并发问题的利器。面对这些工具类,死记硬背 API 是远远不够的。我们需要理解其背后的原理,才能在面试和实际应用中游刃有余。本文将结合生活案例,深入剖析常见的 Java 并发工具类,助你轻松应对面试。
1. CountDownLatch:倒计时器,生活中的“发令枪”
CountDownLatch 允许一个或多个线程等待其他线程完成操作。可以将它想象成运动会上的发令枪。主线程(裁判)等待所有运动员(子线程)准备就绪后,才能开始比赛。
底层原理:
CountDownLatch 内部维护一个计数器,该计数器初始化为一个正整数。当一个线程调用 countDown() 方法时,计数器减 1。当计数器变为 0 时,所有等待的线程都被释放。await() 方法用于阻塞当前线程,直到计数器变为 0。
代码示例:
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 numberOfThreads = 3; // 模拟 3 个线程
CountDownLatch latch = new CountDownLatch(numberOfThreads); // 初始化计数器
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
final int threadId = i + 1;
executor.submit(() -> {
try {
System.out.println("线程 " + threadId + " 正在准备...");
Thread.sleep((long) (Math.random() * 3000)); // 模拟准备时间
System.out.println("线程 " + threadId + " 准备完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 计数器减 1
}
});
}
latch.await(); // 主线程等待所有子线程完成
System.out.println("所有线程准备完毕,开始执行!");
executor.shutdown(); // 关闭线程池
}
}
避坑经验:
- 确保每个线程都会调用
countDown()方法,否则主线程会一直阻塞。 - 如果计数器的初始值为 0,则
await()方法会立即返回,不会阻塞。
2. CyclicBarrier:循环栅栏,团队协作的“集合点”
CyclicBarrier 允许一组线程互相等待,直到所有线程都到达一个公共屏障点。可以将它想象成团队协作中的集合点,每个成员到达后,才能一起开始下一步工作。
底层原理:
CyclicBarrier 内部维护一个计数器,初始值为 parties(参与线程的数量)。当一个线程调用 await() 方法时,计数器减 1。当计数器变为 0 时,所有等待的线程都被释放,并执行 barrierAction(可选)。之后,计数器重置为 parties,进入下一个循环。
代码示例:
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 numberOfThreads = 3; // 模拟 3 个线程
CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {
System.out.println("所有线程到达栅栏,开始下一步操作!");
}); // 初始化 CyclicBarrier,并设置 barrierAction
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
final int threadId = i + 1;
executor.submit(() -> {
try {
System.out.println("线程 " + threadId + " 正在执行第一阶段...");
Thread.sleep((long) (Math.random() * 3000)); // 模拟执行时间
System.out.println("线程 " + threadId + " 第一阶段执行完毕,等待其他线程...");
barrier.await(); // 等待其他线程到达栅栏
System.out.println("线程 " + threadId + " 正在执行第二阶段...");
Thread.sleep((long) (Math.random() * 3000)); // 模拟执行时间
System.out.println("线程 " + threadId + " 第二阶段执行完毕!");
barrier.await(); // 等待其他线程到达栅栏,进入下一次循环
} catch (Exception e) {
e.printStackTrace();
}
});
}
executor.shutdown(); // 关闭线程池
}
}
避坑经验:
- 注意处理
BrokenBarrierException异常,该异常表示其中一个线程中断,导致所有等待线程都无法继续执行。 CyclicBarrier可以重用,而CountDownLatch只能使用一次。
3. Semaphore:信号量,停车场的“停车位”
Semaphore 用于控制同时访问特定资源的线程数量。可以将它想象成停车场的停车位。每个线程需要获取一个许可(停车位),才能访问资源。当线程释放许可时,其他等待的线程才能获取许可。
底层原理:
Semaphore 内部维护一个计数器,表示可用许可的数量。acquire() 方法尝试获取许可,如果计数器大于 0,则计数器减 1,线程继续执行;否则,线程阻塞,直到有其他线程释放许可。release() 方法释放许可,计数器加 1,并唤醒一个等待的线程。
代码示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int numberOfThreads = 5; // 模拟 5 个线程
int permits = 2; // 模拟 2 个停车位
Semaphore semaphore = new Semaphore(permits); // 初始化 Semaphore
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
final int threadId = i + 1;
executor.submit(() -> {
try {
semaphore.acquire(); // 获取许可,相当于进入停车场
System.out.println("线程 " + threadId + " 获取许可,开始访问资源...");
Thread.sleep((long) (Math.random() * 3000)); // 模拟访问资源的时间
System.out.println("线程 " + threadId + " 访问资源完毕,释放许可!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可,相当于离开停车场
}
});
}
executor.shutdown(); // 关闭线程池
}
}
避坑经验:
- 确保
acquire()和release()方法成对出现,否则可能会导致许可泄漏。 - 可以使用
tryAcquire()方法尝试获取许可,如果获取不到则立即返回,避免阻塞。
4. Exchanger:交换器,相亲的“红娘”
Exchanger 允许两个线程交换数据。可以将它想象成相亲的红娘,她负责将男女双方的信息进行交换。
底层原理:
Exchanger 内部维护一个槽位,用于存储一个线程传递的数据。当第一个线程调用 exchange() 方法时,它将数据放入槽位,并阻塞等待第二个线程。当第二个线程调用 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.submit(() -> {
String data = "线程 1 的数据";
try {
System.out.println("线程 1 准备交换数据:" + data);
String exchangedData = exchanger.exchange(data);
System.out.println("线程 1 交换后的数据:" + exchangedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.submit(() -> {
String data = "线程 2 的数据";
try {
System.out.println("线程 2 准备交换数据:" + data);
String exchangedData = exchanger.exchange(data);
System.out.println("线程 2 交换后的数据:" + exchangedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.shutdown();
}
}
避坑经验:
Exchanger只能用于两个线程之间的数据交换。- 如果一个线程等待时间过长,可能会抛出
TimeoutException异常。
掌握了这些 Java 并发工具类的原理和使用方法,相信你就能在面试中脱颖而出,并在实际开发中编写出更加高效、稳定的并发程序。记住,理解底层原理比死记硬背 API 更重要。
冠军资讯
代码一只喵