并发编程之多线程间通信方式及应用场景
通过这篇文章你将了解到并发编程中多线程间通信的定义是什么,实现方式有哪些,应用场景有哪些,以及在 java 语言环境中这些通信方式具体应该怎么实现,都有哪些实现方式等等。

目录
前言:
多线程通信是指多个线程之间为了共同完成同一个任务,通过某种机制进行数据交换和协作的过程。在任务期间它们需要相互传递消息、协调执行顺序、共享资源、避免资源竞争等。典型业务场景有生产者-消费者模式、主从/任务分解与合并模式、流水线/多阶段任务模式、实时数据处理、资源池管理等。
⚠️ 注意
文中会涉及到大量关于 java 并发包 JUC 中的相关知识,若你不了解她或对她有兴趣则可查看历史文章 《JUC》万万万万字长文解析!,文中详细介绍了线程、内存模型、锁、并发编程工具、并发编程应用等知识,不骗你,真的有四万字!
1 概念及核心目的
1.1 多线程通信的概念
多线程通信是指多个线程之间为了共同完成同一个任务,通过某种机制进行数据交换和协作的过程。在任务期间它们需要相互传递消息、协调执行顺序、共享资源、避免资源竞争等。
1.2 多线程通信的目的
- 协调执行顺序:控制多个线程执行的先后顺序;
- 共享数据安全:安全地访问和修改共享资源;
- 线程状态通知:通知或接收其它线程状态的变化;
- 提高系统效率:合理利用 CPU 资源以提高系统效率;
2 典型业务场景
2.1 生产者-消费者模式
生产者-消费者模式是指生产者生产数据,消费者消费数据,生产动作与消费动作相分离。其多线程通信的需求在于生产者通知消费者有新数据产生。常见的应用场景如:
- 消息队列处理:多个生产者生产数据,多个消费者消费数据。
- 日志收集系统:多个线程产生日志,专门的线程消费日志并将其写入磁盘。
2.2 主从/任务分解与合并模式
主从或任务分解与合并模式是指主线程将一个大任务分解为多个小任务,并分配给多个子线程或工作线程执行,待所有子线程都执行完毕后再由主线程汇总执行结果。其多线程通信的需求在于主线程分配任务,子线程或工作线程返回执行结果。常见的应用场景如:
- 并行计算:多个工作线程并行执行计算,主线程汇总计算结果。
- MapReduce 计算:主线程分配任务给工作线程,等待所有结果汇总。
- 大文件处理:多线程分块处理文件,最后合并结果。
- Web 服务器请求处理:Web 服务线程接收请求,分配给多个应用线程处理,最后将结果汇总给 Web 服务线程。如 tomcat 线程与 jvm 线程。
2.3 流水线/多阶段任务模式
流水线或多阶段任务模式是指某个复杂任务可拆分成一系列有先后关系的小任务执行,即任务分阶段执行。其多线程通信的需求在于上一个阶段完成后通知下一个阶段。常见的应用场景如:
- 电商订单处理:创建 -> 支付 -> 发货。
- 媒体文件处理:读取 -> 处理 -> 压缩 -> 保存。
2.4 实时数据处理
实时数据处理是指多个线程分工明确的处理不同的实时数据。其多线程通信的需求常与业务逻辑密切相关。常见的应用场景如:
- 系统状态监控:监控线程监控系统各类状态信息,并根据要求通知其它线程做出响应,如报警线程报警等。
- 股票交易系统:数据接收线程、计算线程、UI 更新线程之间的协作。
- 游戏服务器:网络 IO 线程、逻辑处理线程、数据持久化线程间的通信。
2.5 资源池管理
资源池管理是指为了便于管理资源、节省资源创建销毁所带来的开销而采用的池化技术,其核心在于资源的复用。其多线程通信的需求在于任务提交线程与工作线程之间的协作。常见的应用场景如:
- 数据库连接池:多个线程共享有限的数据库连接。
- 线程池:利用线程池中已创建的线程执行任务。
3 Java 中线程间通信实现
3.1 生产者-消费者模式
3.1.1 BlockingQueue 实现
直接利用阻塞队列的特性实现,是最简洁、最简单的实现。BlockingQueue 即阻塞队列,是 juc 包提供的并发容器,基于 Lock 锁实现。
private final BlockingQueue<Object> queue = new ArrayBlockingQueue<>(10);
public void produce(Object o) throws InterruptedException {
queue.put(o); // 当队列满了时 生产者线程会自动阻塞
}
public Object consume() throws InterruptedException {
return queue.take(); // 当队列为空时 消费者线程会自动阻塞
}
3.1.2 synchronized + wait/notify 实现
当队列满了时,用 wait() 阻塞生产者线程,消息入队后用 notifyAll() 唤醒等待的消费者线程;当队列为空时,用 wait() 阻塞消费者线程,取出消息后用 notifyAll() 唤醒生产者线程。用 synchronized 关键字保证整个操作的原子性。是最经典的实现方式。
synchronized 是 java 提供的一个并发控制关键字,其可以解决原子性问题、可见性问题、有序性问题。可同步方法,亦可同步代码块,被修饰的方法或代码块在同一时刻只能被一个线程调用。其实现原理为 Monitor 和 CAS 算法,Monitor 底层基于 C++ 的 ObjectMonitor 实现。
private final Queue<Object> queue = new LinkedList<>();
private final int capacity = 10;
public synchronized void produce(Object o) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 队列满时 生产者等待
}
queue.offer(o);
notifyAll(); // 入队后通知消费者
}
public synchronized Object consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 队列空时 消费者等待
}
Object o = queue.poll();
notifyAll(); // 出队后通知生产者
return o;
}
3.1.3 ReentrantLock + Condition 实现
使用可重入独占锁 ReentrantLock 和锁条件变量 Condition 实现。用两个条件变量表示队列 满 与 空,用锁保证整个操作的原子性。
其中锁 Lock 是 juc 包中提供的锁相关行为的接口,结合 AQS(AbstractQueuedSynchronizer) 实现,比 synchronized 隐式锁更加灵活,扩展性更强。主要实现类有 ReentrantLock-可重入独占锁、ReentrantReadWriteLock-可重入读写锁,这些实现同时支持阻塞式与非阻塞式、公平与非公平。
锁条件变量 Condition 在功能上类似于 Monitor 中的 wait() 与 notify() 的作用对象,由内部的 await() 与 signal() 来实现等待/阻塞和唤醒操作。其内部维护了一个以 Node 对象为节点的单向链表,表示正在等待的线程,类似于 Monitor 中的 WaitSet 集合,修改头尾节点时用 CAS 算法。AQS 的等待可以等待在多个条件变量上(即每个 Condition 对象),而 synchronized 只能等待在同一个锁对象上。
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 满
private final Condition notEmpty = lock.newCondition(); // 空
private final Object[] items = new Object[10];
private int putIndex, takeIndex, count;
public void produce(Object o) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 等待不满条件
}
items[putIndex] = o;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal(); // 通知不空
} finally {
lock.unlock();
}
}
public Object consume() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 等待不空条件
}
Object o = items[takeIndex];
takeIndex = (takeIndex + 1) % items.length;
count--;
notFull.signal(); // 通知不满
return o;
} finally {
lock.unlock();
}
}
3.2 主从/任务分解与合并模式
3.2.1 ExecutorService + Future 实现
最常用的实现方式,适合大多数场景,但不支持复杂的任务依赖场景。
ExecutorService 是 juc 人提供的线程池相关接口,继承自 Executor,增加了操作线程池的方法定义,如 shutdown() 、shutdownNow() 、isShutdown() 等,同时还增加了更多提交任务或线程池执行任务的方法定义,如 submit()、invokeAll()、invokeAny() 等。Future 是 juc 提供的用来同步接收任务执行结果的接口,其基于保护性暂停模式实现。
// 创建定长线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 主任务:计算 1 到 1000 的平方和
int totalNumbers = 1000;
int workerCount = 5;
int chunkSize = totalNumbers / workerCount;
// 存储子任务的计算结果
List<Future<Long>> futures = new ArrayList<>();
// 主任务分解任务
for (int i = 0; i < workerCount; i++) {
final int start = i * chunkSize + 1;
final int end = (i == workerCount - 1) ? totalNumbers : (i + 1) * chunkSize;
// 子任务执行任务
Future<Long> future = executor.submit(() -> {
long sum = 0;
for (int j = start; j <= end; j++) {
sum += (long) j * j;
}
return sum;
});
futures.add(future);
}
// 主任务合并结果
long totalSum = 0;
for (Future<Long> future : futures) {
totalSum += future.get();
}
executor.shutdown();
System.out.println(totalSum);
3.2.2 ForkJoinPool 实现
适用于递归分解的任务,如归并排序、大数据计算等。可以充分利用 CPU,缺点是不适合 IO 密集型任务。
ForkJoinPool 是 jdk1.7 加入的一种线程池实现,它采用一种分治思想,适用于可进行任务拆分的业务场景。ForkJoinPool 会将多个小任务的执行交给多个线程去执行,但是任务的拆分需要自己实现,且其默认会创建和 CPU 核数相同的线程数。
public class SumTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 100; // 阈值
private final int[] array;
private final int start;
private final int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
// 小于阈值 直接计算
if (length <= THRESHOLD) {
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
}
// 大于阈值 分解任务
int mid = start + length / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
leftTask.fork(); // 异步执行左任务
long rightResult = rightTask.compute(); // 同步执行右任务
long leftResult = leftTask.join(); // 等待左任务完成
return leftResult + rightResult; // 合并结果
}
}
int[] array = new int[1000];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
ForkJoinPool pool = new ForkJoinPool();
SumTask task = new SumTask(array, 0, array.length);
System.out.println(pool.invoke(task));
3.2.3 CompletableFuture 实现
适用于复杂的异步任务编排和组合。CompletableFuture 是 jdk8 引入的类,用于支持异步编程和非阻塞操作,它是 Future 的扩展,提供了更加强大的功能,如回调、组合编排多个异步任务等。其支持异步编程能力、函数式编程、组合与转换等。又一次证明了大哥李的强大!
public class CompletableFutureTest {
static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 步骤一:下载数据
CompletableFuture<String> downloadFuture = CompletableFuture.supplyAsync(() -> {
sleep(3000);
return "data";
});
// 步骤二:并行处理数据
CompletableFuture<String> process1 = downloadFuture.thenApplyAsync(data -> {
sleep(1000); // 处理任务一
return "result1";
});
CompletableFuture<String> process2 = downloadFuture.thenApplyAsync(data -> {
sleep(2000); // 处理任务二
return "result2";
});
// 步骤三:合并结果
CompletableFuture<String> combinedFuture = process1.thenCombine(process2, (r1, r2) -> {
return r1 + "_" + r2;
});
System.out.println(combinedFuture.get());
}
}
主从/任务分解与合并模式除了以上三种实现方式外,还可以使用 CountDownLatch + Thread 实现,其适用于需要精确控制等待的场景。
3.3 流水线/多阶段任务模式
3.3.1 BlockingQueue 链式连接实现
最常用的实现方式,通过多个阻塞队列连接各个阶段,上一个阶段的输出阻塞队列是下一个阶段的输入阻塞队列。优点是清晰易懂,易扩展,各阶段解耦,天然的背压机制(队列满时阻塞);缺点是内存占用较大。
// 定义阶段接口
@FunctionalInterface
public interface Stage<I, O> {
O process(I input);
}
// 实现流水线阶段
public class PipelineStage<I, O> implements Runnable {
private BlockingQueue<I> inputQueue; // 当前阶段输入
private BlockingQueue<O> outputQueue; // 当前阶段输出
private Stage<I, O> stage; // 当前阶段动作
private String stageName; // 当前阶段名称
private volatile boolean running = true;
public PipelineStage(BlockingQueue<I> inputQueue, BlockingQueue<O> outputQueue, Stage<I, O> stage, String stageName) {
this.inputQueue = inputQueue;
this.outputQueue = outputQueue;
this.stage = stage;
this.stageName = stageName;
}
@Override
public void run() {
try {
while (running) {
I input = inputQueue.poll(1000, TimeUnit.MICROSECONDS);
if (input == null) {
continue;
}
O output = stage.process(input);
if (output == null || outputQueue == null) {
continue;
}
outputQueue.put(output);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void shutdown() {
running = false;
}
}
// 流水线使用示例:图片处理流水线
public class ImagePipeline {
static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
// 各阶段数据存储队列
BlockingQueue<String> readQueue = new LinkedBlockingQueue<>();
BlockingQueue<byte[]> decodeQueue = new LinkedBlockingQueue<>();
BlockingQueue<byte[]> processQueue = new LinkedBlockingQueue<>();
BlockingQueue<byte[]> processedQueue = new LinkedBlockingQueue<>();
BlockingQueue<String> saveQueue = new LinkedBlockingQueue<>();
// 阶段一:根据图片路径读取图片
Stage<String, byte[]> read = path -> {
sleep(100); // 假装读取图片
return path.getBytes();
};
// 阶段二:解码图片
Stage<byte[], byte[]> decode = data -> {
sleep(200); // 假装解码图片
return new String(data).getBytes();
};
// 阶段三:图片处理(如添加滤镜、缩放等)
Stage<byte[], byte[]> process = data -> {
sleep(300); // 假装处理图片
return new String(data).getBytes();
};
// 阶段四:保存图片
Stage<byte[], String> save = data -> {
sleep(100); // 假装保存图片
return "path";
};
// 创建流水线各阶段
PipelineStage<String, byte[]> stage1 = new PipelineStage<>(readQueue, decodeQueue, read, "读取阶段");
PipelineStage<byte[], byte[]> stage2 = new PipelineStage<>(decodeQueue, processQueue, decode, "解码阶段");
PipelineStage<byte[], byte[]> stage3 = new PipelineStage<>(processQueue, processedQueue, process, "处理阶段");
PipelineStage<byte[], String> stage4 = new PipelineStage<>(processedQueue, saveQueue, save, "保存阶段");
// 启动所有阶段任务
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(stage1);
executor.submit(stage2);
executor.submit(stage3);
executor.submit(stage4);
// 输入最初数据
for (int i = 0; i < 5; i++) {
readQueue.put("image" + i + ".png");
}
sleep(3000); // 等待处理完成
// 关闭
stage1.shutdown();
stage2.shutdown();
stage3.shutdown();
stage4.shutdown();
}
3.3.2 CompletableFuture 链式调用实现
适合异步流水线,每个阶段可以是异步操作。优点是异步非阻塞、链式调用简洁、支持分支合并;缺点是不适合大数据流、调试困难。
// 普通流水线
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> pipeline = CompletableFuture.supplyAsync(() -> {
sleep(100); // 阶段一:抽取/获取数据
return "extract_data";
}, executor).thenApplyAsync(data -> {
sleep(200); // 阶段二:清洗数据
return "wash_data";
}, executor).thenApplyAsync(data -> {
sleep(300); // 阶段三:转换数据
return "transform_data";
}, executor).thenApplyAsync(data -> {
sleep(100); // 阶段四:保存数据
return "save_data";
}, executor).exceptionally(e -> {
sleep(100); // 异常处理
return "e";
});
String result = pipeline.join();
executor.shutdown();
System.out.println(result);
// 批量处理流水线
ExecutorService executor = Executors.newFixedThreadPool(8);
// 创建多个数据的流水线
CompletableFuture<?>[] futures = new CompletableFuture[10];
for (int i = 0; i < 10; i++) {
futures[i] = CompletableFuture.supplyAsync(() -> {
// todo
return "data";
}, executor).thenApplyAsync(data -> {
// todo
return data;
}, executor).thenApplyAsync(data -> {
// todo
return data;
}, executor).thenAccept(result -> {
// todo 输出结果
});
}
// 等待所有流水都完成
CompletableFuture.anyOf(futures).join();
executor.shutdown();
// 分支与合并流水线
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// todo
return "data";
}).thenApplyAsync(data -> {
// todo 分支处理1
return "data1";
}).thenCombineAsync(CompletableFuture.supplyAsync(() -> {
// todo 分支处理2
return "data2";
}), (result1, result2) -> {
// todo 合并结果
return "data1" + "data2";
});
System.out.println(future.join());
3.3.3 Reactor/RxJava 响应式流水线实现
适用于复杂的异步数据流处理,需要添加依赖 io.projectreactor:reactor-core,优点是强大的数据流处理能力、丰富的操作符;缺点是学习曲线陡峭、调试困难。
除了上述三种实现方式外,java Stream API 也属于流水线 API,可用于轻型流水线任务处理,如数据转换与处理。此外,还有一个非常牛逼的高性能流水线框架 Disruptor,是 LMAX 开源的线程间消息传递框架,其无锁设计提供了超高性能,适用于极致性能场景,但配置较复杂,学习较困难。
3.4 实时数据处理
实时数据处理的核心挑战是:高吞吐量、低延迟、数据一致性、容错能力等,这方面的设计已经相当成熟,如:
BlockingQueue + 多消费模式;Kafka + 多线程消费;CompletableFuture 异步实时处理;Reactor 响应式实时处理或Disruptor 高性能实时处理;
3.5 资源池管理
资源池管理的核心挑战是:有效控制有限资源的分配、复用和回收,避免频繁重复创建销毁带来的开销。如常用的线程池 ExecutorService 所提供的各种类型的线程池实现,以及 ThreadPoolExecutor ;数据库连接池 HiKariCP 等。在设计时可使用 juc 提供的并发容器(如 BlockingQueue、ConcurrentHashMap 等) + 原子类(如 AtomicInteger、AtomicLong 等)+ 关键字 volatile 等组件实现。
⚠️ 注意
文中会涉及到大量关于 java 并发包 JUC 中的相关知识,若你不了解她或对她有兴趣则可查看历史文章 《JUC》万万万万字长文解析!,文中详细介绍了线程、内存模型、锁、并发编程工具、并发编程应用等知识,不骗你,真的有四万字!


被折叠的 条评论
为什么被折叠?



