news 2026/4/15 5:52:08

技术演进中的开发沉思-350:并发模型(上)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
技术演进中的开发沉思-350:并发模型(上)

今天说的线程通信是并发编程的核心问题之一,本质是解决多线程协作时的信息交换问题—— 多线程并非孤立执行,若要协同完成复杂任务(如生产者 - 消费者、任务流水线),就必须通过特定机制交换信息,而共享内存模型消息传递模型是线程通信的两大根本设计思路,Java 原生采用共享内存模型作为核心通信方式,消息传递模型则作为重要补充在开发中广泛使用。

这两种模型的核心差异在于是否存在公共共享状态通信是隐式还是显式,且直接决定了并发程序的设计思路、同步方式和问题排查重点。下面结合你的核心要点,补充核心原理、Java 中的实现方式、优缺点、典型场景,同时明确两者的核心区别和实际开发中的使用策略。

一、线程通信的意义

在并发编程中,单独的线程执行无意义,多线程的价值在于协作完成单线程无法高效处理的任务(如高并发请求、批量数据处理),而线程协作的前提就是线程间能准确、安全地交换信息

  • 比如生产者线程生产数据后,需要通知消费者线程进行消费;
  • 比如任务执行线程完成计算后,需要将结果传递给结果汇总线程。

线程通信的两大模型,本质是解决 **“如何让线程间交换信息”的两种不同方案,其设计思路的差异,直接导致了同步机制、并发问题、开发复杂度 ** 的根本不同。

二、Java 原生采用,基于内存读写的隐式通信

这是Java 并发编程的核心通信模型,也是 JVM 的默认实现,同时也是 Java 中可见性、原子性、有序性等并发问题的根源—— 因为通信依赖共享内存的读写,若不做特殊规范,多线程的读写操作会出现无序、不可见等问题。

核心原理

  1. 线程间存在公共的共享内存区域(Java 中主要是堆内存,因为堆是所有线程共享的,虚拟机栈 / 本地方法栈为线程私有);
  2. 线程通过读写共享内存中的公共状态(共享变量)完成通信,无需显式的 “发送 / 接收” 消息,通信过程是隐式的;
  3. 线程间的同步(何时读、何时写)需要开发者手动保证—— 即需要通过锁、volatile 等机制,控制线程对共享变量的访问时序,避免出现脏读、脏写、不可见等问题。

Java 内存模型(JMM)

Java 的共享内存模型并非直接对应物理内存,而是通过Java 内存模型(JMM)做了一层抽象封装 ——JMM 规范了线程如何访问共享内存何时将线程私有工作内存中的数据同步到主内存,其核心目标就是解决共享内存模型下的可见性、原子性、有序性问题,为线程间的隐式通信提供安全保障。

JMM 的核心抽象:

  • 主内存:存储所有共享变量,对应物理内存的一部分;
  • 工作内存:每个线程独有的私有内存,存储共享变量的副本,对应 CPU 的寄存器 / 高速缓存;
  • 线程对共享变量的所有操作,必须先在工作内存中执行,再同步回主内存,不能直接操作主内存。

Java 中共享内存模型的具体实现方式

所有基于共享变量读写的通信方式,都属于共享内存模型,这是 Java 开发中最常用的方式,核心实现依赖 JMM 的同步机制:

  1. synchronized + 共享变量:通过 synchronized 保证共享变量读写的原子性、可见性、有序性,是最基础的实现方式;
  2. volatile + 共享变量:通过 volatile 保证共享变量的可见性、有序性(无法保证原子性),适用于简单的状态标记(如停止线程的 flag);
  3. JUC 原子类(AtomicXxx) + 共享变量:通过 CAS 实现共享变量的原子性读写,同时保证可见性,适用于简单的数值型共享状态;
  4. ReentrantLock + 共享变量:与 synchronized 类似,功能更丰富,适用于复杂的同步场景。

synchronized 实现生产者 - 消费者(共享内存模型)

通过共享变量count(库存)作为通信媒介,生产者和消费者通过读写count隐式通信,用 synchronized 保证同步:

public class ShareMemoryDemo { // 共享状态:库存(堆内存中的共享变量,通信媒介) private int count = 0; private final Object lock = new Object(); private static final int MAX = 10; // 生产者:写共享变量count public void produce() { synchronized (lock) { while (count >= MAX) { try { lock.wait(); } catch (InterruptedException e) {} } count++; System.out.println(Thread.currentThread().getName() + "生产,库存:" + count); lock.notifyAll(); // 通知其他线程:共享变量已修改 } } // 消费者:读共享变量count public void consume() { synchronized (lock) { while (count <= 0) { try { lock.wait(); } catch (InterruptedException e) {} } count--; System.out.println(Thread.currentThread().getName() + "消费,库存:" + count); lock.notifyAll(); // 通知其他线程:共享变量已修改 } } public static void main(String[] args) { ShareMemoryDemo demo = new ShareMemoryDemo(); // 生产者线程 new Thread(() -> { for (int i = 0; i < 20; i++) demo.produce(); }, "生产者1").start(); // 消费者线程 new Thread(() -> { for (int i = 0; i < 20; i++) demo.consume(); }, "消费者1").start(); } }

通信逻辑:生产者修改count后,通过notifyAll()唤醒消费者;消费者读取count判断是否可消费,全程无显式的 “消息发送”,仅通过读写共享变量完成通信。

优缺点

优点
  1. 通信效率高:直接读写内存,无需额外的消息封装 / 传递开销;
  2. 开发灵活:可自由定义共享状态的结构和读写逻辑;
  3. 贴合 Java 底层:是 Java 原生支持的模型,与 JVM、JMM 深度融合。
缺点
  1. 并发问题突出:需要手动保证可见性、原子性、有序性,易出现脏读、脏写、死锁等问题;
  2. 开发复杂度高:复杂场景下,同步机制的设计和实现难度大,易出错;
  3. 调试困难:隐式通信导致问题排查难度高,无法直观追踪信息的传递过程。

三、消息传递模型

这是线程通信的另一核心思路,也是对 Java 共享内存模型的重要补充 —— 因无公共共享状态,天然避免了共享内存模型的所有并发问题,是开发中实现多线程协作的更安全、更简单的方式

核心原理

  1. 线程间不存在任何公共的共享状态,每个线程拥有自己的私有数据,彼此完全隔离;
  2. 线程间通过显式的 “发送消息” 和 “接收消息”完成通信,通信过程是显式的;
  3. 同步是内置的,无需开发者手动保证 —— 消息的发送方会等待接收方确认(或消息被接收),接收方会等待消息的到来,天然实现了 “读写时序” 的同步。

核心设计思想

“不要通过共享内存来通信,而要通过通信来共享内存”—— 这是 Go 语言的经典设计理念,也是消息传递模型的核心,彻底颠倒了共享内存模型的思路:不再让线程共享数据,而是让线程通过消息传递来交换数据,从根源上避免共享数据带来的并发问题。

Java 中消息传递模型的具体实现方式

Java 虽原生采用共享内存模型,但 JUC 包提供了丰富的消息传递模型实现,核心基于阻塞队列(BlockingQueue),这是开发中最常用的方式:

  1. BlockingQueue(核心):如 ArrayBlockingQueue、LinkedBlockingQueue,通过 “入队 / 出队” 实现消息的发送 / 接收,内置阻塞和唤醒机制,天然同步;
  2. Future/FutureTask:线程通过 Future 提交任务并获取执行结果,本质是 “任务消息” 的传递;
  3. CompletableFuture:对 Future 的增强,支持异步消息传递和结果回调;
  4. 分布式场景补充:如 MQ(RocketMQ、Kafka),是跨进程 / 跨机器的消息传递模型,本质与线程间的消息传递一致。

BlockingQueue 实现生产者 - 消费者(消息传递模型)

通过阻塞队列作为消息队列,生产者向队列中发送消息(入队数据),消费者从队列中接收消息(出队数据),全程无共享变量,同步由 BlockingQueue 内置实现:

import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class MessagePassingDemo { // 消息队列:存储生产的消息(数据),作为通信媒介 private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // 生产者:发送消息(入队) public void produce() throws InterruptedException { int data = (int) (Math.random() * 100); queue.put(data); // 队列满则阻塞,内置同步 System.out.println(Thread.currentThread().getName() + "生产消息:" + data + ",队列大小:" + queue.size()); } // 消费者:接收消息(出队) public void consume() throws InterruptedException { Integer data = queue.take(); // 队列空则阻塞,内置同步 System.out.println(Thread.currentThread().getName() + "消费消息:" + data + ",队列大小:" + queue.size()); } public static void main(String[] args) { MessagePassingDemo demo = new MessagePassingDemo(); // 生产者线程 new Thread(() -> { for (int i = 0; i < 20; i++) { try { demo.produce(); } catch (InterruptedException e) {} } }, "生产者1").start(); // 消费者线程 new Thread(() -> { for (int i = 0; i < 20; i++) { try { demo.consume(); } catch (InterruptedException e) {} } }, "消费者1").start(); } }

通信逻辑:生产者通过put()显式发送消息,消费者通过take()显式接收消息,队列的阻塞 / 唤醒由 JUC 内置实现,开发者无需手动处理任何同步。

优缺点

优点
  1. 天然线程安全:无共享状态,从根源上避免了可见性、原子性、有序性问题;
  2. 开发简单:同步机制由框架内置实现,无需开发者手动编写锁、wait/notify 等代码;
  3. 调试友好:显式的消息发送 / 接收,可直观追踪信息的传递过程,问题排查简单;
  4. 扩展性好:可轻松扩展为跨进程、跨机器的通信(如 MQ),设计思路一致。
缺点
  1. 通信效率略低:消息的封装、入队、出队存在一定的开销,比直接读写共享内存慢;
  2. 灵活性稍差:消息的结构和传递方式受限于框架(如 BlockingQueue 的类型),不如共享内存灵活。

四、共享内存模型 vs 消息传递模型 核心对比

这是并发编程的高频考点,也是实际开发中选择通信方式的关键依据,从核心特征、同步方式、并发问题等维度做清晰对比:

对比维度共享内存模型(Java 原生)消息传递模型(Java JUC 实现)
核心特征有公共共享状态,通过读写内存通信无公共共享状态,通过显式发送 / 接收消息通信
通信方式隐式(无需显式消息操作,仅读写变量)显式(必须调用发送 / 接收方法,如 put/take)
同步方式手动同步(需锁、volatile、原子类等)内置同步(框架自动实现阻塞 / 唤醒,无需手动)
并发问题存在可见性、原子性、有序性问题,易出现脏读 / 死锁无共享状态,天然避免所有共享内存相关并发问题
通信效率高(直接读写内存,无额外开销)稍低(消息封装、入队出队有开销)
开发复杂度高(需手动处理同步,易出错)低(框架封装,只需调用 API)
调试难度高(隐式通信,难以追踪信息传递)低(显式通信,消息流转可直观追踪)
Java 核心实现synchronized、volatile、AtomicXxx、ReentrantLockBlockingQueue、Future、CompletableFuture
核心设计理念共享内存来通信通信来共享内存

五、结合两种模型,扬长避短

Java 并非只能用某一种模型,实际开发中通常结合两种模型的优势,用消息传递模型做核心协作,用共享内存模型做局部轻量通信,既保证开发效率和线程安全,又兼顾性能:

  1. 优先使用消息传递模型:对于多线程的核心协作场景(如生产者 - 消费者、任务分发、结果汇总),优先使用BlockingQueueCompletableFuture等消息传递方式 —— 开发简单、安全,能大幅减少并发 bug;
  2. 局部使用共享内存模型:对于简单的、轻量的状态通信(如线程的停止标记、简单的数值统计),使用volatileAtomicXxx等共享内存方式 —— 通信效率高,代码简洁;
  3. 用消息传递模型封装共享内存:即使需要使用共享内存,也可通过消息传递模型封装(如将共享变量放入阻塞队列,通过消息传递来修改),减少直接的共享内存访问。

典型场景:高并发业务处理

  • ThreadPoolExecutor(线程池)做任务分发:主线程通过execute()向线程池提交任务(消息传递模型,任务是消息);
  • 线程池内的工作线程处理任务时,若需要简单的状态同步(如处理成功数统计),用AtomicInteger共享内存模型)做轻量统计;
  • 处理结果通过CompletableFuture返回给主线程(消息传递模型)。

最后小结

  1. 线程通信是并发编程的核心问题,目的是解决多线程协作的信息交换,分为共享内存模型消息传递模型两大根本思路;
  2. 共享内存模型是 Java 原生实现,通过读写堆内存的共享变量隐式通信,需手动保证同步,效率高但开发复杂、易出并发 bug,JMM 是其核心规范;
  3. 消息传递模型是 Java 的重要补充,通过显式的发送 / 接收消息通信,无共享状态,同步由框架内置实现,开发简单、天然安全,核心实现是BlockingQueue
  4. 两者核心差异在于是否有共享状态、通信是否显式、同步是否手动,共享内存模型的核心是 “共享内存来通信”,消息传递模型的核心是 “通信来共享内存”;
  5. 实际开发中优先使用消息传递模型做核心协作,局部轻量场景使用共享内存模型,结合两者优势,既安全又高效。

线程通信的两种模型,是并发编程的设计思想基石—— 理解这两种模型,能让你从根本上理解 Java 中各种并发工具的设计初衷,也能让你在实际开发中更合理地选择协作方式,编写更安全、高效的并发代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/9 2:53:58

基于Java的影视创作论坛的设计与实现(11880)

有需要的同学&#xff0c;源代码和配套文档领取&#xff0c;加文章最下方的名片哦 一、项目演示 项目演示视频 二、资料介绍 完整源代码&#xff08;前后端源代码SQL脚本&#xff09;配套文档&#xff08;LWPPT开题报告&#xff09;远程调试控屏包运行 三、技术介绍 Java…

作者头像 李华
网站建设 2026/4/9 1:42:03

uni-app 之 设置导航

uni-app 提供了一系列 API 来动态设置页面导航栏的样式和状态&#xff0c;帮助开发者创建更丰富的用户界面体验。 1. uni.setNavigationBarTitle(OBJECT) 动态设置当前页面的标题 参数说明 属性类型必填说明titlestring是页面标题successfunction否接口调用成功的回调函数f…

作者头像 李华
网站建设 2026/4/11 15:40:17

光刻胶用屏蔽剂

一、光刻胶对光屏蔽剂性能的具体详细要求光屏蔽剂的核心作用是控制光在光刻胶膜中的传播行为&#xff0c;其主要性能要求可归纳为以下几点&#xff1a;精确的光学特性&#xff08;核心要求&#xff09;在曝光波长下具有高吸收系数&#xff08;α&#xff09;&#xff1a;这是最…

作者头像 李华