当前位置: 首页 > news >正文

并发编程的源头

1.1. 并发编程的全景图:三个核心问题

1. 分工 —— 提高并发性能的关键

  • 含义:合理分配任务给多个线程,就像项目经理分配工作。
  • 目标:提升程序执行效率。
  • 实现工具和模式
    • Java SDK 并发包中的工具:
      • Executor
      • Fork/Join
      • Future
    • 并发设计模式:
      • 生产者-消费者模式
      • Thread-Per-Message 模式
      • Worker Thread 模式
  • 学习建议
    • 类比现实场景,例如“厨师做菜 - 服务员上菜”说明生产者-消费者模型。

2. 同步 —— 实现线程间协作

  • 含义:一个线程完成任务后,通知其他线程继续。
  • 目标:线程之间有序协作,避免混乱。
  • 常见技术
    • 异步调用与 Future 的配合(通过 get() 实现等待与通知)
    • 协作工具类:
      • CountDownLatch
      • CyclicBarrier
      • Phaser
      • Exchanger
  • 底层机制
    • 管程(Monitor):线程协作的理论基础。
  • 常见协作场景举例
    • 生产者 - 消费者模型中的“等待”和“唤醒”。

3. 互斥 —— 保证线程安全

  • 含义:多个线程访问共享资源时保证操作的正确性。
  • 三大线程安全问题
    • 可见性
    • 有序性
    • 原子性
  • 解决方案
    • Java 内存模型(JMM):解决可见性、有序性问题。
    • 互斥(锁):解决原子性问题。
      • 常见锁工具:
        • synchronized
        • ReentrantLock
        • ReadWriteLock
        • StampedLock
    • 无锁方案
      • 原子类(如 AtomicInteger 等)
      • Copy-On-Write(写时复制)
      • ThreadLocalfinal变量等。
  • 注意问题
    • 性能开销
    • 死锁风险
  • 理论基础需补充
    • CPU 缓存一致性
    • 操作系统原语
    • 原子操作底层原理(如 CAS)

1.2. JVM( Java Virtual Machine )

JVM是一个虚构出来的计算机,一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现

JVM 其实就类似于一台小电脑运行在 windows 或者 linux 这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。

运行过程:

  1. Java 文件经过编译后变成 .class 字节码文件字
  2. 节码文件通过类加载器被搬运到 JVM 虚拟机中
  3. 虚拟机主要的 5 大块:方法区,堆都为线程共享区域,有线程安全问题,栈和本地方法栈和计数器都是独享区域,不存在线程安全问题,而 JVM 的调优主要就是围绕堆,栈两大块进行

JVM 执行机制(以方法调用为例)

假设你调用一个 Java 方法,JVM 的运行机制如下:

  1. 类加载器 找到对应 .class 文件并加载进 JVM。
  2. 方法区 存储类的结构(字段、方法、常量池等)。
  3. 创建对象实例。
  4. 栈帧(Stack Frame) 被压入线程的 Java 栈,记录该方法执行过程。
  5. 程序计数器 保存当前线程所执行的字节码指令地址。
  6. 执行引擎 解析字节码或使用 JIT 编译后直接执行本地代码。
  7. 如需调用 C/C++ 方法,则通过 JNI 接口进入本地方法栈。
  8. 方法执行完后,栈帧被销毁,返回结果。
  9. 如果对象生命周期结束,GC 检测并清理其内存。

2. 可见性、原子性和有序性问题:并发编程Bug的源头

并发问题三要素:

特性

问题来源

表现

可见性

CPU 缓存

值更新对其他线程不可见

原子性

线程切换

操作中断导致数据错误

有序性

编译优化

执行顺序颠倒引发异常

写并发程序的建议:

  1. 理解底层机制:并发问题源于系统性能优化,了解 CPU、内存、编译器行为至关重要;
  2. 掌握解决方案:如使用 volatilesynchronized、并发包等;
  3. 调试有方法:抓住“可见性 / 原子性 / 有序性”三个点去分析并发 Bug。

2.1. 并发Bug背后的根本原因

并发问题的本质:硬件与软件设计之间的不一致性和优化带来的副作用

三大差异导致的核心矛盾:

组件

相对速度

CPU

快如“天上一天”

内存

慢如“地上一年”

I/O

更慢如“地上十年”

为缓解三者矛盾,系统层面做了三件事:

  1. CPU → 加缓存
  2. OS → 引入进程/线程分时复用
  3. 编译器 → 指令重排序优化

它们提升了性能,但同时也带来了并发 Bug 的根源

2.2. 并发Bug的三大源头

1. 缓存导致的可见性问题

单核 vs 多核缓存模型:

  • 单核 CPU:线程共享缓存,写入立刻对其他线程可见。
  • 多核 CPU:每个核有自己的缓存,写入后不会自动同步到其他缓存,产生“脏读”现象。

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性

可见性关键词:

  • 缓存副本
  • 不一致性
  • 写后不立即同步
  • volatile 可用于解决部分可见性问题

2. 线程切换带来的原子性问题

操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。

在一个时间片内,如果一个进程进行一个 IO 操作,例如读个文件,这个时候该进程可以把自己标记为“休眠状态”并出让 CPU 的使用权,待文件读进内存,操作系统会把这个休眠的进程唤醒,唤醒后的进程就有机会重新获得 CPU 的使用权了。

这里的进程在等待 IO 时之所以会释放 CPU 使用权,是为了让 CPU 在这段等待时间里可以做别的事情,这样一来 CPU 的使用率就上来了;此外,如果这时有另外一个进程也读文件,读文件的操作就会排队,磁盘驱动在完成一个进程的读操作后,发现有排队的任务,就会立即启动下一个读操作,这样 IO 的使用率也上来了。

早期的操作系统基于进程来调度 CPU,不同进程间是不共享内存空间的,所以进程要做任务切换就要切换内存映射地址,而一个进程创建的所有线程,都是共享一个内存空间的,所以线程做任务切换成本就很低了。现代的操作系统都基于更轻量的线程来调度,现在我们提到的“任务切换”都是指“线程切换”

count += 1 背后的 CPU 操作:

  1. 从内存读取 count 到寄存器;
  2. 在寄存器中加一;
  3. 写回内存或CPU cache。

如果在线程 A 执行完步骤 1 后被切换,线程 B 也执行这三步,会导致 最终结果丢失更新

我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。

原子性关键词:

  • 线程上下文切换
  • 指令级非原子
  • Java 提供 synchronized / AtomicLong 等保证原子性

3. 编译优化带来的有序性问题

编译器重排序:

  • 出于性能考虑,编译器会调整代码顺序(只要最终结果不变)
  • 但在并发场景下,顺序错乱可能导致 Bug

经典案例:双重检查锁单例

public class Singleton {static Singleton instance;static Singleton getInstance(){if (instance == null) {synchronized(Singleton.class) {if (instance == null)instance = new Singleton();}}return instance;}
}

问题出在 new 操作执行顺序的优化:

  • 正确顺序:
  1. 分配内存;
  2. 初始化对象;
  3. 将地址赋值给 instance
  • 实际优化后可能:
  1. 分配内存;
  2. 将地址赋值给 instance
  3. 初始化对象;
  • 如果线程 B 在步骤 2 后读取 instance,它会以为对象已初始化,从而导致 空指针异常

有序性关键词:

  • 指令重排
  • volatile 可部分禁止重排序
  • Java 内存模型(JMM)
http://www.lqws.cn/news/72685.html

相关文章:

  • 探索 Dify 的工作流:构建智能应用的新范式
  • Flink CDC将MySQL数据同步到数据湖
  • Kafka集成Flume/Spark/Flink(大数据)/SpringBoot
  • 【设计模式-3.6】结构型——桥接模式
  • React 组件异常捕获机制详解
  • 打卡第34天:MLP神经网络训练
  • 4、ubuntu系统 | 文本和目录操作函数
  • react 生命周期
  • Java 2D 图形类总结与分类
  • 自定义Shell命令行解释器
  • 数据结构哈希表总结
  • [SC]SystemC中常用的宏和小工具
  • 抛砖引玉:RadarDet4D,NuScenes数据集Radar模态目标检测第二名(即将开源)
  • uniapp-商城-77-shop(8.2-商品列表,地址信息添加,级联选择器picker)
  • 3. TypeScript 中的数据类型
  • Linux磁盘管理
  • 业务到解决方案构想
  • SQL 中的 `CASE WHEN` 如何使用?
  • 达梦数据库 Windows 系统安装教程
  • CentOS8.3+Kubernetes1.32.5+Docker28.2.2高可用集群二进制部署
  • 状态机实现文件单词统计
  • 人工智能在智能制造业中的创新应用与未来趋势
  • HealthBench医疗AI评估基准:技术路径与核心价值深度分析(上)
  • 架构师面试题整理
  • VitalInsight智能体检报告解读
  • 【深度学习-Day 21】框架入门:神经网络模型构建核心指南 (Keras PyTorch)
  • 每天总结一个html标签——a标签
  • CMake指令:string(字符串操作)
  • Linux--进程概念
  • 车载诊断架构 --- DTC消抖参数(Trip Counter DTCConfirmLimit )