当前位置: 首页 > 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.xdnf.cn/news/766279.html

相关文章:

  • Flink CDC将MySQL数据同步到数据湖
  • C++ 标准输入输出 -- <iostream>
  • 【深度学习新浪潮】多模态模型如何处理任意分辨率输入?
  • LazyOwn RedTeam/APT 框架是第一个具有人工智能驱动的 CC 的 RedTeam 框架
  • 6.linux文本内容显示cat,more,less
  • 第七部分:第五节 - 数据关系与进阶查询 (TypeORM):仓库里复杂的配料组合
  • 第1篇:数据库中间件概述:架构演进、典型方案与应用场景
  • 微服务常用日志追踪方案:Sleuth + Zipkin + ELK
  • SCAU8642--快速排序
  • C++ 内存泄漏检测器设计
  • 7.文本内容处理sort,uniq,out,cat,comm,diff
  • NX869NX874美光固态颗粒NX877NX883
  • [HTML5]快速掌握canvas
  • 在 Linux 服务器上无需 sudo 权限解压/打包 .7z 的方法
  • C++ - 数据处理之数值转不同进制的字符串(数值转十进制字符串、数值转八进制字符串、数值转二进制字符串、数值转十六进制字符串)
  • 黑马程序员C++核心编程笔记--4 类和对象--多态
  • 《信号与系统》--期末总结V1.0
  • linux 的devmem2 调式使用说明
  • Vue-3-前端框架Vue基础入门之VSCode开发环境配置和Tomcat部署Vue项目
  • 常见ADB指令
  • Vue-4-前端框架Vue基础入门之Vue的常用操作
  • opencv调用模型
  • 渗透实战PortSwigger Labs AngularJS DOM XSS利用详解
  • 【MySQL】视图与用户管理
  • linux——文件系统
  • 使用API网关Kong配置反向代理和负载均衡
  • IoTGateway项目生成Api并通过swagger和Postman调用
  • Fisher准则例题——给定类内散度矩阵和类样本均值
  • 数据库系统概论(十六)数据库安全性(安全标准,控制,视图机制,审计与数据加密)
  • 好用的C/C++/嵌入式 IDE: CLion的下载安装教程(保姆级教程)