前言
并发是Java程序设计中比较重要和难以理解的一个方面。要编写正确高效的多线程程序,除了要理解Java线程的基本概念和线程安全问题外,还需要熟练使用JDK提供的各种并发工具类。
CountDownLatch
实现原理
CountDownLatch是一个非常有用的工具,它可以让一个或多个线程等待其他线程完成任务后再继续执行。具体来说,CountDownLatch内部维护了一个计数器,当计数器值为0时,所有等待的线程都可以继续执行。
使用CountDownLatch非常简单,只需要调用其await()方法等待其他线程完成任务,或者调用其countDown()方法通知CountDownLatch计数器减1即可。
下面是一个使用CountDownLatch实现多线程并发任务的示例:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2); // 创建一个CountDownLatch对象,初始计数器值为2
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "正在执行任务1...");
latch.countDown(); // 计数器减1
}, "Thread1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "正在执行任务2...");
latch.countDown(); // 计数器减1
}, "Thread2").start();
latch.await(); // 等待计数器值为0
System.out.println("所有任务执行完成!");
}
}
在上面的示例中,我们创建了一个CountDownLatch对象,并将计数器初始值设置为2。然后,我们启动了两个线程分别执行任务1和任务2,并在每个任务执行完毕后调用了CountDownLatch的countDown()方法,将计数器值减1。
最后,我们调用了CountDownLatch的await()方法,让当前线程等待计数器值为0。当计数器值为0时,所有等待的线程都可以继续执行。
应用场景
CountDownLatch有非常广泛的应用场景,主要可以归纳为:
- 确保某个任务的多个依赖都准备就绪之后再继续执行。例如启动一个服务,需要数据库、网络等多个依赖启动完毕之后再继续。
- 实现最大并行度控制。例如,某个任务有10个子任务,但我们每次只想同时执行3个,等待一个完成再启动一个新的。
- 死锁避免。比如启动多个线程,但其中只有一个可以获取资源,其他线程需要等待。如果没有CountDownLatch控制,很容易死锁
- 主线程等待所有子线程完成。这也是上面例子展示的场景。主线程需要等待多个子线程完成任务之后再继续执行。这里给出一些具体应用案例:
- 云服务启动场景:
网络服务启动
数据库服务启动
HR服务启动
主线程调用latch.await(),等待所有服务启动完毕,再继续启动业务服务。 - 测试场景:
启动多个线程跑测试
每个线程跑完Tests之后countDown()
主线程调用latch.await(),等待所有测试完成,汇总测试报告 - 定时任务场景:
启动多个定时任务线程
每个线程定时执行任务,执行完后countDown()
主线程调用latch.await(time),等待所有线程在time时间内完成,否则超时处理 - 爬虫场景:
启动多个线程爬取网页
每个线程爬完一个网页后countDown()
主线程调用latch.await(),等待所有网页爬取完毕,关闭爬虫程序
CountDownLatch适用于线程之间的同步,比如某个线程要等待若干个其他线程完成各自的工作后才能执行
Semaphore
实现原理
Semaphore也是一个非常有用的工具,它可以控制同时访问某个资源的线程数量。
简单来说大致原理:- 初始化一个许可证数量,如5个
- 每个acquire()方法消费一个许可证,许可证减1
- 每个release()方法增加一个许可证,许可证加1
- 当许可证为0时,acquire()方法会阻塞线程
- 直到有release()方法增加许可证,被阻塞的线程才获得许可证,继续执行
使用Semaphore也非常简单,只需要调用其acquire()方法获取许可证,或者调用其release()方法释放许可
证即可。下面是一个使用Semaphore实现多线程并发访问共享资源的示例:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); // 创建一个Semaphore对象,初始许可证数量为2
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个许可证
System.out.println(Thread.currentThread().getName() + "正在访问共享资源...");
Thread.sleep(1000);
semaphore.release(); // 释放一个许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread1").start();
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个许可证
System.out.println(Thread.currentThread().getName() + "正在访问共享资源...");
Thread.sleep(1000);
semaphore.release(); // 释放一个许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread2").start();
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个许可证
System.out.println(Thread.currentThread().getName() + "正在访问共享资源...");
Thread.sleep(1000);
semaphore.release(); // 释放一个许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread3").start();
}
}
在上面的示例中,我们创建了一个Semaphore对象,并将许可证数量初始值设置为2。然后,我们启动了三个线程分别访问共享资源,并在访问之前先调用Semaphore的acquire()方法获取许可证,在访问完成后调用Semaphore的release()方法释放许可证。
应用场景
- 限制数据库连接数
- 限制IO密集型任务线程数
- 限制访问公共资源的线程数
- 用作线程池的bsize功能等
Semaphore适用于限制可以访问某组资源(物理或逻辑的)的线程数目
Exchanger
Exchanger是一个线程交换数据的工具类。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
实现原理
它的主要原理是:- 两个线程通过exchange()方法交换数据
- 第一个调用exchange()的线程会阻塞,等待第二个线程调用exchange()
- 当第二个线程调用exchange()后,两个线程就可以交换彼此的数据,并同时释放阻塞继续执行
使用Exchanger也非常简单,只需要创建一个Exchanger对象,并在两个线程中分别调用其exchange()方法即可。下面是一个使用Exchanger实现多线程数据交换的示例:
import java.util.concurrent.Exchanger;
public class ExchangerExample {
public static void main(String[] args) throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data = "Hello";
System.out.println(Thread.currentThread().getName() + "正在交换数据:" + data);
String result = exchanger.exchange(data); // 等待另一个线程交换数据
System.out.println(Thread.currentThread().getName() + "交换到的数据:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread1").start();
new Thread(() -> {
try {
String data = "World";
System.out.println(Thread.currentThread().getName() + "正在交换数据:" + data);
String result = exchanger.exchange(data); // 等待另一个线程交换数据
System.out.println(Thread.currentThread().getName() + "交换
应用场景
- 两个线程交换消费的数据,例如一个线程生成数据,一个线程消费数据,通过Exchanger交换数据
- 两个线程相互传递消息,通过Exchanger达到线程间通信的目的。
- 线程池中,工作线程交换结果数据给主线程。
- 两个线程交换计算结果,最后得到两个线程计算结果的融合值。
Exchanger适用于两个线程之间需要交换数据,并继续各自执行的场景
CyclicBarrier
实现原理
- 初始化一个循环屏障,并指定一定数量的线程需要到达屏障(barrierAction)
- 多个线程调用await()方法到达屏障
- 第一个到达的线程会阻塞等待,其他线程继续到达
- 当最后一个线程到达屏障时,所有阻塞的线程会被唤醒,并执行barrierAction指定的任务
- 屏障持续循环,线程继续在屏障处阻塞和被唤醒执行
CyclicBarrier和CountDownLatch的区别在于,CyclicBarrier可以重复使用,即在所有线程都到达屏障点之后,所有线程会被释放并可以继续执行下一轮的任务。下面是一个使用CyclicBarrier实现多线程任务分解的示例:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) throws InterruptedException {
int n = 4;
CyclicBarrier cyclicBarrier = new CyclicBarrier(n, () -> {
System.out.println("所有线程已到达屏障点,开始执行下一轮任务...");
});
for (int i = 0; i < n; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "正在执行任务...");
Thread.sleep(1000);
cyclicBarrier.await(); // 等待其他线程到达屏障点
System.out.println(Thread.currentThread().getName() + "继续执行任务...");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "Thread" + i).start();
}
}
}
在上面的示例中,我们创建了一个CyclicBarrier对象,并设置参与线程的数量为4,当所有线程都到达屏障点后,执行的操作是输出一行提示信息。然后,我们启动了4个线程,每个线程都会执行一个任务,并在任务执行完成后调用CyclicBarrier的await()方法等待其他线程到达屏障点。
应用场景
CyclicBarrier的主要应用场景有:
- 并行测试,等待所有线程完成一个测试 iteration 后再继续2
- 并行计算,每个线程处理输入数据的一部分,然后在屏障处等待所有线程完成,最后合并运算结果
- 并行算法迭代,每个线程处理输入数据的一部分,在屏障处等待所有线程,再继续下一轮迭代计算
- 多个线程在屏障处等待,主线程发信号让所有线程继续执行
CyclicBarrier适用于一定数量的线程互相等待,然后再继续执行的场景。它提供了一个循环的同步点,使线程可以在这个点循环等待和继续执行。
总结
在Java并发编程中,JDK提供了许多用于实现线程同步和协作的工具类,例如CountDownLatch、Semaphore、Exchanger和CyclicBarrier等。这些工具类都是基于Java并发包中的同步器(synchronizer)实现的,使用起来非常方便,可以有效地提高多线程程序的性能和可靠性。