并发编程是Java程序中的一大挑战。幸好,Java为我们提供了一系列的并发工具,帮助我们更轻松地管理和控制多线程程序。本文将深入探讨三个常用的并发工具:CountDownLatch
、CyclicBarrier
和Semaphore
。
1. CountDownLatch
1.1 概念与应用场景
CountDownLatch
是一个同步工具类,它允许一个或多个线程等待其他线程完成某些操作。它的工作原理是:当创建一个CountDownLatch
时,你会指定一个计数器。线程调用countDown()
方法会减少这个计数器,而await()
方法则会阻塞,直到计数器为零。
应用场景:
- 确保某个服务启动后,再启动依赖它的其他服务。
- 等待直到所有任务完成后,再继续执行。
1.2 示例代码
假设我们有一个场景:主线程需要等待三个子线程完成他们的任务后,才能继续执行。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 完成任务.");
latch.countDown();
}, "线程" + i).start();
}
latch.await();
System.out.println("所有子线程已完成任务,主线程继续执行.");
}
}
2. CyclicBarrier
2.1 概念与应用场景
CyclicBarrier
是另一个同步工具,它允许多个线程互相等待,直到它们都到达一个预定的点,然后继续执行。它的名字“Cyclic”是因为它可以重用。
应用场景:
- 分阶段的并发任务,每个阶段完成后,所有线程都同步,并在所有线程都准备好后继续下一阶段。
2.2 示例代码
假设我们有一个场景:四个旅行者需要过河,但是只有一艘小船,每次只能承载两个旅行者。旅行者们需要相互合作,确保每次都有两个人才能划船过河。
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(2, () -> {
System.out.println("两位旅行者都已准备好,开始过河...");
});
for (int i = 1; i <= 4; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 已到达河边.");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, "旅行者" + i).start();
}
}
}
这样,每当有两个旅行者到达河边,他们就会一起过河,直到所有旅行者都过河为止。
3. Semaphore
3.1 概念与应用场景
Semaphore
(信号量)是一个经典的并发工具,它用于控制同时访问特定资源的线程数量。简单来说,信号量维护了一个许可集合。为了访问受限资源或进入一个受限区,线程必须从信号量获取一个许可。当线程完成对资源的访问后,它必须将许可返回给信号量。
应用场景:
- 限制同时访问资源的线程数量,例如数据库连接。
- 控制并发线程数以限制资源使用。
3.2 示例代码
假设我们有一个场景:有一个公共的停车场,只有三个停车位。我们有六辆车尝试在这个停车场里停车。
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
// 定义一个信号量,允许3个许可(也即3个停车位)
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
// 获取许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获得停车位.");
Thread.sleep(2000); // 模拟停车2秒钟
System.out.println(Thread.currentThread().getName() + " 离开停车位.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
}
}, "车辆" + i).start();
}
}
}
在上述代码中,六辆车尝试停在只有三个停车位的停车场。只有当一个车辆离开其停车位时,另一辆车才能进入并占用那个位置。Semaphore
为我们提供了acquire()
和release()
方法,让我们可以控制资源的访问。
4. 对比与总结
-
CountDownLatch:是一次性的阻塞,不可重用。主要用于让一组线程等待某个事件发生后再执行。
-
CyclicBarrier:可重复使用,让一组线程互相等待,直到它们都达到一个预定的点。
-
Semaphore:控制并发线程数,用于资源的并发访问控制。
虽然这三个工具类有各自的用途和特点,但它们都提供了简洁和强大的机制来帮助开发人员控制并发和同步问题。
在实际开发中,选择使用哪种并发工具取决于你要解决的问题和具体场景。理解每个工具的内部机制和适用场景,可以帮助你更好地利用Java的并发库,编写高效、可靠的多线程应用。
以上是关于Java中的CountDownLatch
、CyclicBarrier
和Semaphore
的深度解析。这三个并发工具都是Java并发库中的重要组成部分,深入了解和恰当地使用它们,将有助于你编写出更加高效和稳健的并发代码。
5. 其他重要的Java并发工具
除了上述三种工具,Java并发库还提供了其他几个重要的工具,这些工具也在解决并发问题中起到了关键作用。下面简要介绍一些常见的并发工具,并提供示例代码。
5.1 Future & Callable
在Java中,我们通常使用Runnable
接口来创建一个可以并行执行的任务。但是,Runnable
无法返回结果或抛出检查型异常。为了解决这个问题,Java并发库引入了Callable
接口和Future
类。
Callable
与Runnable
类似,但可以返回结果或抛出异常。而Future
则代表了Callable
任务的结果。
示例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureCallableDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> callable = () -> {
Thread.sleep(2000); // 模拟一些工作
return "Callable任务的结果";
};
Future<String> future = executor.submit(callable);
try {
String result = future.get(); // 获取并等待Callable的结果
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
5.2 Exchanger
Exchanger
是一个同步工具,用于在两个线程之间交换数据。它提供了一个同步点,在这个点,两个线程可以交换其各自的数据。
示例代码:
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data1 = "线程1的数据";
System.out.println(Thread.currentThread().getName() + ": 交换前的数据为 - " + data1);
data1 = exchanger.exchange(data1);
System.out.println(Thread.currentThread().getName() + ": 交换后的数据为 - " + data1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程1").start();
new Thread(() -> {
try {
String data2 = "线程2的数据";
System.out.println(Thread.currentThread().getName() + ": 交换前的数据为 - " + data2);
data2 = exchanger.exchange(data2);
System.out.println(Thread.currentThread().getName() + ": 交换后的数据为 - " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程2").start();
}
}
5.3 Phaser
Phaser
是一个灵活且可重用的同步工具,与CyclicBarrier
和CountDownLatch
类似,但提供了更加动态的方式来处理注册的线程数。
示例代码:
import java.util.concurrent.Phaser;
public class PhaserDemo {
public static void main(String[] args) {
Phaser phaser = new Phaser(1); // 主线程也需要注册
for (int i = 0; i < 3; i++) {
phaser.register(); // 注册新的参与者
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": 执行任务...");
phaser.arriveAndAwaitAdvance(); // 等待所有线程到达
System.out.println(Thread.currentThread().getName() + ": 继续执行...");
}, "线程" + (i + 1)).start();
}
phaser.arriveAndDeregister(); // 主线程准备好后取消注册
}
}
以上仅为Java并发库中的几个工具类的概述和示例,实际上库中还有许多其他功能强大的工具和类,如ReentrantLock
, Condition
, ScheduledExecutorService
等。在实际开发中,根据需求选择合适的工具很关键。希望这篇文章能帮助你更好地理解和使用Java并发库。