并发编程的源头

article/2025/7/4 21:59:09

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.hkcw.cn/article/iUTIAoblVj.shtml

相关文章

【Linux】Git原理与使用

编程不仅是解决问题的艺术,更是对复杂性进行优雅管理的哲学。 前言 这是我自己学习Git工具的笔记。后期我会继续把Git工具笔记开源至博客上。 版本控制器Git 1. Git是一款去中心化的分布式版本控制系统。 2. Git提交时,仅会记录并提交文件的变动部分。 G…

“等待-通知”机制优化(一次性申请)循环等待

1. “等待-通知”机制优化(一次性申请)循环等待 等待‑通知 释放锁 阻塞 唤醒 重新抢锁 所有 wait/notify 都属于 锁对象 的等待队列。 用 notifyAll(),写成 while(…) wait() —— 黄金法则。 面对自旋消耗 CPU 的场景,优…

麒麟信安安装谷歌浏览器

参考文档 麒麟信安系统Chrome离线安装包:高效便捷的浏览器解决方案-CSDN博客 项目文件预览 - 麒麟信安系统Chrome离线安装包:本仓库提供了一个适用于麒麟信安系统的Chrome浏览器离线安装包。该安装包包含了所有必要的依赖文件,并且已经对系统中已有的依…

智启未来:当知识库遇见莫奈的调色盘——API工作流重构企业服务美学

目录 引言 一、初识蓝耘元生代MaaS平台 1.1 平台架构 1.2 平台的优势 1.3 应用场景 二、手把手教你如何在蓝耘进行注册 (1)输入手机号,将验证码正确填入即可快速完成注册 (2)进入下面的页面表示已经成功注册&…

LangGraph framework

目录 Agent 架构Router(路由)工具调用代理(Tool-calling Agent)工具调用记忆机制(Memory)规划机制(Planning) 自定义 Agent 架构人类参与(Human-in-the-loop)…

零基础开始的网工之路第十七天------计算机网络知识

目录 1、以太网MAC地址 2.交换机的工作模式 2.1、交换机以太网接口双工模式 2.2、交换机以太网接口速率 4.模式间的转换 5.命令行的层次关系 6.命令行帮助 7.常用命令 8.交换机以太网接口的工作模式配置 二.路由器配置 三.TCP协议 2.TCP的建立连接和断开连接 2.1建…

核心机制:滑动窗口

TCP 协议 1.确认应答 可靠传输的核心机制 2.超时重传 可靠传输的核心机制 3.连接管理 TCP/网络 最高的面试题 三次握手,建立连接(必须是 三次) 四次挥手,断开连接(可能是 三次) 核心机制四:滑动窗口 算法中的"滑动窗口" 出自 TCP 前面的三个…

JAVA核心知识点--元注解详解

📚博客主页:代码探秘者 ✨专栏:《JavaSe》 其他更新ing… ❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️ 🙏作者水平有限,欢迎各位大佬指点&…

微软markitdown PDF/WORD/HTML文档转Markdown格式软件整合包下载

本次和大家分享另一个微软发布的非常热门的文件文档转Markdown格式文档的软件markitdown,软件可以将PDF,word,ppt,Excel等十几种格式文档转换为markdown格式文档,我基于当前最新0.1.2版本制作了免安装一键启动整合包。…

(八)登录认证与学生写作画像

本次将赵昱琨同学之前完成的学生写作画像与智能学习路径规划的后端与目前已有的后端框架进行整合。同时为了实现学生写作画像与智能学习路径规划,需要在之前简易的登录系统上进行重构,所以本次大规模重写了登录模块,同时发现很多过去冗余的代…

【agent开发】部署LLM(一)

本周基本就是在踩坑,没什么实质性的进展 下载模型文件 推荐一个网站,可以简单计算下模型推理需要多大显存:https://apxml.com/tools/vram-calculator 我的显卡是RTX 4070,有12GB的显存,部署一个1.7B的Qwen3应该问题…

【Linux】pthread多线程基础

参考博客:https://blog.csdn.net/Alkaid2000/article/details/128121066 线程概述 与进程类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份…

瑞萨CS+ for CC V8.13.00环境安装教程

前言:最近接触到瑞萨的芯片,需要安装对应的集成开发环境,发现这与ARM内核的单片机存在很大的不同,这里先简单介绍一下其IDE的安装配置方式。 1,官网下载 瑞萨半导体开发环境安装网址 CS | Renesas 当然在下载安装包之…

【知识点】第3章:基本数据类型

文章目录 知识点整理数字类型字符类型 练习题判断题程序题 知识点整理 数字类型 Python语言提供整数、浮点数、复数3种数字类型。 不同进制的引导符号: 不考查进制间的转换。 浮点数类型与数学中实数的概念一致,表示带有小数的数值。Python语言要求所…

【算法】回溯法

一、回溯法的基本思想 回溯法有“通用解题方法”的美称,解题过程是一个搜索过程。在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回(也就是递归返回),尝试别的路径。因此&#xff0…

AIGC 基础篇 高等数学篇 01函数与极限

声明:本文章仅用于博主本人复习,请不要将本文章当成预习篇或者讲解篇 此外,此文章不会包含全部的高等数学知识,仅仅是为了学习AI而进行的前期学习,因此知识含量不会很多,由于博主是第一次尝试做&#xff0…

如何在 Windows 11 Home 版上下载和安装 Hyper-V

Windows 11 Home 版与之前的微软操作系统版本一样,没有自带 Hyper-V 管理器。因此,如果您想在 Windows 11 Home 上下载和安装 Hyper-V,以下是详细的步骤教程。 Hyper-V 是微软提供的一种虚拟化解决方案,允许用户为各种操作系统创建虚拟机。与 VMware 或 VirtualBox 不同,…

C++ --- string类的简单实现

string类的简单实现 前言1、基本成员2、构造方法和析构方法2.1无参构造2.2有参构造2.3析构函数2.4拷贝构造函数 3、遍历方式3.1operator [ ]3.2iterator3.2.1正向迭代器3.2.2const正向迭代器 3.3范围for 4、常用方法,运算符重载c_str()size()reverse()push_back()po…

ESP32之Linux编译环境搭建流程

背景:为了解决 “windows环境中编译ESP32代码速度慢” 的问题,现搭建一个Linux环境,让windows下的VScode连接到Linux环境,VSCode负责编辑代码,虚拟机用于编译代码。 目录 一、安装VMware 1.1 获取VMware安装包 1.2…

Python-matplotlib中的Pyplot API和面向对象 API

matplotlib中的Pyplot API和面向对象 API Pyplot API(状态机模式)面向对象 API 详解二者差别核心区别方法命名差异注意事项差别举例 🍅 Pyplot API(状态机模式)和面向对象 API 是两种不同的编程接口.🍅 它们…