Java内存模型与互斥锁

article/2025/6/28 19:41:07

1. Java内存模型:解决可见性和有序性问题

1.1. 内存模型

内存模型的概念:

内存模型是一个抽象的概念,它描述了计算机内存的组织和访问方式。在编程中,内存模型是编译器和硬件设计者用来优化代码性能和正确性的一种工具。

内存模型主要包括以下几个部分:

1. 内存布局:描述了内存的组织方式,包括代码段、数据段、堆和栈等。

2. 内存访问:描述了如何在内存中读取和写入数据。例如,是按字节访问还是按字访问。

3. 内存同步:描述了在多线程环境中如何同步访问内存。例如,如何保证多个线程看到的内存一致性。

4. 内存管理:描述了对象内存的分配和回收

我们目前重点关注内存同步,是多线程的重点

内存布局:学到各个语言的时候再深入学

内存访问:计组和操作系统的知识,之前学过

内存管理:如何分配对象,回收对象。学到各个语言的时候再深入学。比如:Java、Go、Python有各自的垃圾回收机制

1.2. Java内存模型

为何需要 JMM?

并发 Bug 源头

真实动机

失效表现

CPU 缓存

提升访问速度

可见性 —— 线程 A 的写,线程 B 看不见

编译 / CPU 重排

提升指令吞吐

有序性 —— 语句顺序被打乱

线程切换

平衡 IO 与 CPU

原子性 —— 单条语句被拆分

  • 目标:让程序员“在必要处”按需禁用这些优化,而其余地方依旧高速运行。

java内存模型的概念:

Java内存模型(JMM) = 关键字 (volatile / synchronized / final) + Happens‑Before 规则集

它让我们“按需关闭”缓存与重排,确保 可见性 与 有序性,同时保留大部分优化带来的性能。牢记 “钥匙三件套 + HB 六军规”,并发调 Bug 时就能对症下药。

1. JMM 给程序员的 3 把钥匙

关键字

解决

常见用途

volatile

可见性 & 有序性(单变量)
写→读间插入内存屏障

状态标志、轻量级读–写锁

synchronized/ Lock

可见性 + 排他原子性(临界区)

原子复合操作、条件队列

final

构造期后不可变,可见性保证

不可变对象、枚举、单例字段

2. Happens‑Before(HB)六规则

它表达的意思是:前面一个操作的结果对后续操作是可见的。

#

规则 & 口诀

含义(A HB B ⇒ A 的写对 B 可见)

1

程序顺序(顺)

同一线程,代码先后顺序天然 HB

2

volatile 写→读(volatile)

对同一 volatile变量,写 HB 随后的读

3

传递性(传)

A HB B 且 B HB C ⇒ A HB C

4

锁解→锁加(锁)

同一锁,解锁 HB 之后的加锁

5

线程 start()(启)

线程 A 调 t.start() HB t 线程体

6

线程 join()/结束(等)

t 线程体结束 HB A 中 t.join() 返回

  • 管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。

2. 互斥锁

2.1. 原子性问题的解决办法

原子性问题的源头是线程切换:

概念

1 句话释义

关键陷阱

原子性 (Atomicity)

若一组 CPU 指令 要么全部完成,要么全不做,中途不会被别的线程观察到中间态

线程切换拆散指令序列 → 写一半/读一半

源头

OS 调度靠 CPU 中断,多核下可真“并行执行”

禁中断在多核无用

同一时刻只有一个线程执行”这个条件非常重要,我们称之为互斥

2.2. 锁模型

  • 一把锁 ⇐⇒ 一组受保护资源(1:N)
  • 错误示例:用多把锁守同一资源 → 同步失效

Java 语言提供的锁技术:synchronized

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块,它的使用示例基本上都是下面这个样子:

class X {// 修饰非静态方法synchronized void foo() {// 临界区}// 修饰静态方法synchronized static void bar() {// 临界区}// 修饰代码块Object obj = new Object();void baz() {synchronized(obj) {// 临界区}}
}  
  • 修饰代码块的时候,锁定了一个 obj 对象;
  • 修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
  • 修饰非静态方法的时候,锁定的是当前实例对象 this。

用 synchronized 解决 count+=1 问题

管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

class SafeCalc {long value = 0L;synchronized long get() {return value;}synchronized void addOne() {value += 1;}
}

get() 方法和 addOne() 方法都需要访问 value 这个受保护的资源,这个资源用 this 这把锁来保护。线程要进入临界区 get() 和 addOne(),必须先获得 this 这把锁,这样 get() 和 addOne() 也是互斥的。

2.3. 锁和受保护资源的关系

受保护资源和锁之间的关联关系是 N:1 的关系。

存在并发问题的例子:

class SafeCalc {static long value = 0L;synchronized long get() {return value;}synchronized static void addOne() {value += 1;}
}

改动后的代码是用两个锁保护一个资源。这个受保护的资源就是静态变量 value,两个锁分别是 this 和 SafeCalc.class。

由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。

互斥锁原理:锁,一定有一个要锁定的对象,至于这个锁定的对象要保护的资源以及在哪里加锁 / 解锁,就属于设计层面的事情了。

2.4. 保护没有关联关系的多个资源

银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的。

相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的。

class Account {// 锁:保护账户余额private final Object balLock= new Object();// 账户余额  private Integer balance;// 锁:保护账户密码private final Object pwLock= new Object();// 账户密码private String password;// 取款void withdraw(Integer amt) {synchronized(balLock) {if (this.balance > amt){this.balance -= amt;}}} // 查看余额Integer getBalance() {synchronized(balLock) {return balance;}}// 更改密码void updatePassword(String pw){synchronized(pwLock) {this.password = pw;}} // 查看密码String getPassword() {synchronized(pwLock) {return password;}}
}

当然,我们也可以用一把互斥锁来保护多个资源,例如我们可以用 this 这一把锁来管理账户类里所有的资源:账户余额和用户密码。具体实现很简单,示例程序中所有的方法都增加同步关键字 synchronized 就可以了。

但是用一把锁有个问题,就是性能太差,会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的。而我们用两把锁,取款和修改密码是可以并行的。用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,叫细粒度锁

2.5. 保护有关联关系的多个资源

错误方法:

例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。

用户 synchronized 关键字修饰一下 transfer() 方法:

class Account {private int balance;// 转账synchronized void transfer(Account target, int amt){if (this.balance > amt) {this.balance -= amt;target.balance += amt;}} 
}

问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。

正确使用锁:

用 Account.class 作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。

class Account {private int balance;// 转账void transfer(Account target, int amt){synchronized(Account.class) {if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}} 
}

  • 锁能覆盖所有受保护资源

总结:

我们再引申一下上面提到的关联关系,关联关系如果用更具体、更专业的语言来描述的话,其实是一种“原子性”特征,在前面的文章中,我们提到的原子性,主要是面向 CPU 指令的,转账操作的原子性则是属于是面向高级语言的,不过它们本质上是一样的。

原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。


http://www.hkcw.cn/article/XeUhOovXqO.shtml

相关文章

Java如何读取CSV文件并将数据放入对象中详解

Java如何读取CSV文件并将数据放入对象中详解 CSV(Comma-Separated Values)文件是一种常见的数据存储格式,广泛应用于数据交换、日志记录和表格数据处理。在Java开发中,读取CSV文件并将数据映射到对象中是常见的需求。本文将详细介…

LazyOwn RedTeam/APT 框架是第一个具有人工智能驱动的 CC 的 RedTeam 框架

一、软件介绍 文末提供程序和源码下载 LazyOwn RedTeam/APT 框架是第一个具有人工智能驱动的 C&C 的 RedTeam 框架,具有隐藏活动的 rootkit、与 Windows/Linux/Mac OSX 兼容的不可检测的可塑植入物,以及自配置后门。凭借其 Web 界面和强大的…

Java学习——day2(Servlet 基础编程)

文章目录 1. 什么是 Servlet?2. Servlet 的生命周期2.1 init()2.2 service()2.3 destroy() 3. 创建 Servlet 类4. Servlet 配置方式4.1 创建步骤4.2 重新构建并启动项目4.3 启动Tomcat 5. 总结 1. 什么是 Servlet? Servlet 是 Java Web 中用于处理客户端…

Aop + 注解实现数据字典类型转换 EasyExcel导出

Aop 注解 实现数据字典类型转换 文章目录 Aop 注解 实现数据字典类型转换一、基础方式✅字典转换简介👉实现步骤✅ 1. 定义自定义注解Dict ✅ 2. 定义查询字典项的两个方法✅ 3. 定义Aop拦截我们查询的方法✅ 4. VO映射类✅ 5. Controller层✅ 6. serviceImpl✅ 7. …

力扣热题100之对称二叉树

题目 给你一个二叉树的根节点 root , 检查它是否轴对称。 代码 方法一:递归 # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left …

链式前向星图解

e[idx] b; 边之终点 ne[idx] h[a]; 谓之头插之边 h[a] idx ; 谓之指针更新 注意:上述以a为开头的一条链上的结点,在物理上都是a的邻接点,相邻的边用idx来标明序号,相邻的边之间有映射。 链式前向星的遍历 假设顶点 u 的邻接表…

MybatisPlus(含自定义SQL、@RequiredArgsConstructor、静态工具类Db)

大家在日常开发中应该能发现,单表的CRUD功能代码重复度很高,也没有什么难度。而这部分代码量往往比较大,开发起来比较费时。 因此,目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是…

R语言基础| 创建数据集

在R语言中,有多种数据类型,用以存储和处理数据。每种数据类型都有其特定的用途和操作函数,使得R语言在处理各种数据分析任务时非常灵活和强大: 向量(Vector): 向量是R语言中最基本的数据类型,它…

UE特效Niagara性能分析

开启Niagara调试器 开启显示概览 界面显示 🟩 上方绿色面板:Niagara DebugHud 这是 HUD(调试视图) 模式下的性能统计显示,内容如下: 项目含义SystemFilter: ShockWave_01当前选中的 Niagara 粒子系统名称…

MySQL——事务

目录 问题 什么是事务 为什么会有事务 事务版本支持 事务提交方式 事务常见操作 事务隔离级别 查看与设置隔离性 事务四种隔离级别 读未提交 读提交 不可重复读 串行化 一致性 理解隔离性 4个隐藏字段 undo日志 MVCC Read View RR 与 RC 本质区别 问题…

Vue-3-前端框架Vue基础入门之VSCode开发环境配置和Tomcat部署Vue项目

文章目录 1 安装配置VSCode1.1 安装中文语言插件1.2 主题颜色1.3 禁用自动更新1.4 开启代码提示设置1.5 安装open in browser插件2 安装配置nodejs2.1 配置环境变量2.2 npm与maven的区别2.3 使用npm避坑3 创建Vue项目3.1 两种创建方式3.2 package.json3.3 安装新的依赖3.4 运行…

Webpack依赖

Webpack到底怎么对我们的项目进行打包捏? 在webpack处理应用程序时,会根据命令或者配置文件找到入口文件 从入口开始,会生成一个依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(.js、css文件、图片、字体…

关于找不到符号,符号变量log的问题的解决方案

创建时间:06月02日 1、问题描述 最近在学习鱼皮的AI超级智能体项目过程中,遇到如下错误: java: 找不到符号符号: 变量 log位置: 类 com.liucc.aiagent.app.LoveApp2、排查思路 网上的常见解决思路主要是检查lombok插件是否安装、Enable…

mysql离线安装教程

1.下载地址: https://downloads.mysql.com/archives/community/ 2.上传安装包到系统目录,并解压 tar -xvf mysql-8.0.34-1.el7.x86_64.rpm-bundle.tar3.检查系统中是否存在mariadb的rpm包 rpm -qa|grep mariadb存在则删除 rpm -e xxx4.解压完后执行如下命令安装 sudo rpm -iv…

【git-首次初始化本地项目、关联远程仓库】

使用场景: windows系统本地首次创建的项目本地已存在但未关联为git项目 操作步骤 创建远程仓库 在gitee或github创建新仓库即可本地安装git 搜索“git安装教程”,按照步骤安装即可打开git中端,进入项目文件夹 cd E:\xxx备注:这…

AI驱动的文本转Mermaid图表工具Smart Mermaid

简介 什么是 Smart Mermaid ? Smart Mermaid 是一款基于 AI 技术的 Web 应用程序,能够将文本内容智能转换为 Mermaid 格式的代码,并将其渲染成可视化图表。用户只需输入文本描述,AI 即可生成相应的图表,支持多种图表类…

调用蓝耘API打造AI 智能客服系统实践教程

声明:文章是实验教程,不是广告 1.前言 在用户与人工客服的沟通中,等待时间长、需求难满足等问题频发,企业面临用户流失风险,用户渴望快速精准的答案,企业需要“开源节流”、“降本增效”。对此&#xff0c…

VM图像处理之图像二值化

什么是灰度? 灰度(Grayscale)是指将彩色图像转换为仅包含亮度信息(黑白过渡)的单通道图像的过程或结果。灰度图像中每个像素的数值代表该点的明暗程度,而不包含颜色信息。 亮度代替颜色: 灰度…

通信革新与网络安全探索与创新:开启未来之门

在科技飞速发展的当下,各领域的前沿探索正不断刷新着人类的认知与能力边界。脑机接口领域取得重大突破,上海阶梯医疗科技有限公司成功完成国内首例侵入式脑机接口系统前瞻性临床试验,受试者通过植入大脑的设备实现用意念玩游戏,为…

sigmastar实现SD卡升级

参考文章:http://wx.comake.online/doc/DD22dk2f3zx-SSD21X-SSD22X/customer/development/software/Px/zh/sys/P3/usb%20&%20sd%20update.html#21-sd 1、构建SD卡升级包 在project下make image完成后使用make_sd_upgrade_sigmastar.sh脚本打包SD卡升级包。 ./make_sd_up…