在上篇 并发编程Bug起源:可见性、有序性和原子性问题,介绍了操作系统为了提示运行速度,做了各种优化,同时也带来数据的并发问题,

定义

在单线程系统中,代码按照顺序从上往下顺序执行,执行不会出现问题。比如一下代码:

int a = 1;
int b = 2;
int c = a + b;

程序从上往下执行,最终c的结果一定会是3

但是在多线程环境中,代码就不一定会顺序执行了。代码的运行结果也有不确定性。在开发中,自己本地没问题,一行行查看代码也没有问题,但是在高并发的生产环境就会出现违背常理的问题。

多线程系统提升性能有如下几个优化:

  • 单核的cpu改成多核的cpu,每个cpu都有自己的缓存。
  • 多个线程可以在cpu线程切换。
  • 代码可能根据编译优化,更新代码的位置。

这些优化会导致可见性原子性以及有序性问题,为了解决上述问题,Java内存模型应运而生。

Java内存模型是定义了Java程序在多线程环境中,访问共享内存和内存同步的规范,规定了线程之间的交互方式,以及线程与主内存、工作内存的的数据交换。

Java内存模型解决并发

导致可见性的原因的是缓存,导致有序性的问题是编译优化,那解决可见性、有序性问题就是禁用缓存和编译优化。这样虽然解决了并发问题,但是性能却下降了。

合理的方案就是按需求禁用缓存和编译优化,在需要的地方添加对应的编码即可。Java内存模型规范了JVM如何按需禁用缓存和编译优化,具体包括volatilesynchronizedfinal这几个关键字,以及Happens-Before规则。

可见性问题

在多核cpu操作系统中每次cpu都有自己的缓存,cpu先从内存获取数据,再进行运算。比如下图中线程A和线程B,分别运行自己的cpu,然后从内存获取变量到自己的cpu缓存中,并进行计算。

线程B改变了变量之后,线程A是无法获取到最新的值。以下代码中,启动两个线程,线程启动完线程A,循环获取变量,如果是true,一直执行循环,直到被改成false才跳出循环,然后再延迟1s启动线程B,线程修改变量值为true:

private static boolean flag = true;

// 线程A一直读取变量flag,直到变量为false,才跳出循环
class ThreadA extends Thread {
@Override
public void run() {
while (flag) {
// flag 为 true,一直读取flag字段,flag 为 false 时跳出来。
//System.out.println("一直在读------" + flag);
}
System.out.println("thread - 1 跳出来了");
}
}
// 1s 后线程B将变量改成 false
class ThreadB extends Thread { @Override
public void run() {
System.out.println("thread-2 run");
flag = false;
System.out.println("flag 改成 false");
}
} @Test
public void test2() throws InterruptedException {
new Thread1().start();
// 暂停一秒,保证线程1 启动并运行
Thread.sleep(1000);
new Thread2().start();
}

运行结果:

thread-2 run
flag 改成 false

线程A一直处于运行中,说明线程B修改后的变量,线程A并未知道。

flag变量添加volatile声明,修改成:

private static volatile boolean  flag = true;

再运行程序,运行结果:

thread-2 run
flag 改成 false
thread - 1 跳出来了

线程B运行完后,线程A也跳出了循环。说明修改了变量后,其他线程也能获取最新的值。

一个未声明volatile的变量,都是从各自的cpu缓存获取数据,线程更新数据之后,其他线程无法获取最新的值。而使用volatile声明的变量,表明禁用缓存,更新数据直接更新到内存中,每次获取数据都是直接内存获取最新的数据。线程之间的数据都是相互可见的。

可见性来自happens-before规则,happens-before用来描述两个操作的内存可见性,如操作Ahappens-before操作B,那么A的结果对于B是可见的,前面的一个操作结果对后续操作是可见的happens-before定义了以下几个规则:

  • 解锁操作happens-before同一把锁的加锁操作。
  • volatile 字段的写操作happens-before同一字段的读操作。
  • 线程的启动操作happens-before该线程的第一个操作。
  • Ahappens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeC。happens-before具有传递性。

有序性问题

先看一个反常识的例子:

int a=0, b=0;
public void method1() {
b = 1;
int r2 = a;
} public void method2() {
a = 2;
int r1 = b;
}

定义了两个共享变量ab,以及两个方法。第一个方法将共享变量b赋值为1 ,然后将局部变量r2赋值为a。第二个方法将共享变量a赋值为2,然后将局部变量r1赋值为b

在单线程环境下,我们可以先调用第一个方法method1,再调用method2方法,最终得到r1r2的值分别为1,0。也可以先调用method2,最后得到r1r2的值分别为0,2

如果代码没有依赖关系,JVM编译优化可以对他们随意的重排序,比如method1方法没有依赖关系,进行重排序:

int a=0, b=0;
public void method1() {
int r2 = a;
b = 1;
} public void method2() {
int r1 = b;
a = 2;
}

此时在多线程环境下,两个线程交替运行method1method2方法:

重排序后r1r2分别是0,0

那如何解决重排序的问题呢?答案就是将变量声明为volatile,比如a或者b变量声明volatile。比如b声明为volatile,此时b的赋值操作要happens-before r1的赋值操作。

int a=0;
volatile int b=0;
public void method1() {
int r2 = a;
b = 1;
} public void method2() {
int r1 = b;
a = 2;
}

同一个线程顺序也满足happens-before关系以及传递性,可以得到r2的赋值happens-before a的赋值。也就表明对a赋值时,r2已经完成赋值了。也就不可能出现r1r200的结果。

内存模型的底层实现

Java内存模型是通过内存屏障来实现禁用缓存和和禁用重排序

内存屏障会禁用缓存,在内存写操作时,强制刷新写缓存,将数据同步到内存中,数据的读取直从内存中读取。

内存屏障会限制重排序操作,当一个变量声明volatile,它就插入了一个内存屏障,volatile字段之前的代码只能在之前进行重排序,它之后的代码只能在之后进行重排序。

总结

Java内存模型(Java Memory Model,JMM)定义了Java程序中多线程之间共享变量的访问规则,以及线程之间的交互行为。它规定了线程如何与主内存和工作内存交互,以确保多线程程序的可见性、有序性和一致性。

  • 可见性:使用volatile声明变量,数据读取直接从内存中读取,更新也是强制刷新缓存,并同步到主内存中。

  • 有序性:使用volatile声明变量,确保编译优化不会重排序该字段。

  • Happens-Before: 前面一个操作的结果对后续操作是可见的

参考

一篇文章告诉你什么是Java内存模型的更多相关文章

  1. 第三章 - CPU缓存结构和java内存模型

    CPU 缓存结构原理 CPU 缓存结构 查看 cpu 缓存 速度比较 查看 cpu 缓存行 cpu 拿到的内存地址格式是这样的 CPU 缓存读 根据低位,计算在缓存中的索引 判断是否有效 0 去内存读 ...

  2. Java 内存模型都不会,就敢在简历上写熟悉并发编程吗

    从 PC 内存架构到 Java 内存模型 你知道 Java 内存模型 JMM 吗?那你知道它的三大特性吗? Java 是如何解决指令重排问题的? 既然CPU有缓存一致性协议(MESI),为什么 JMM ...

  3. Java 理论与实践: 修复 Java 内存模型,第 2 部分(转载)

    在 JSR 133 中 JMM 会有什么改变? 活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型(Java Memory Model, JMM)的公开建议.在本系列文章的 ...

  4. 一篇文章告诉你为何GitHub估值能达20亿美元

    软件开发平台GitHub今日宣布,已获得硅谷多家知名风投2.5亿美元融资,这也让其融资总额达到了3.5亿美元,此轮融资对GitHub的估值约为20亿美元. GitHub有何特别之处? GitHub创立 ...

  5. (第三章)Java内存模型(下)

    一.happens-before happens-before是JMM最核心的概念.对于Java程序员来说,理解happens-before是理解JMM的关键. 1.1 JMM的设计 从JMM设计者的 ...

  6. (第三章)Java内存模型(上)

    一.java内存模型的基础 1.1 并发编程模型的两个关键问题 在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来 ...

  7. 《Java并发编程实战》第十六章 Java内存模型 读书笔记

    Java内存模型是保障多线程安全的根基,这里不过认识型的理解总结并未深入研究. 一.什么是内存模型,为什么须要它 Java内存模型(Java Memory Model)并发相关的安全公布,同步策略的规 ...

  8. 深入理解java虚拟机-第12章Java内存模型与线程

    第12章 Java内存模型与线程 Java内存模型  主内存与工作内存: java内存模型规定了所有的变量都在主内存中,每条线程还有自己的工作内存. 工作内存中保存了该线程使用的主内存副本拷贝,线程对 ...

  9. 第三章 Java内存模型(上)

    本章大致分为4部分: Java内存模型的基础:主要介绍内存模型相关的基本概念 Java内存模型中的顺序一致性:主要介绍重排序和顺序一致性内存模型 同步原语:主要介绍3个同步原语(synchroized ...

  10. 《深入理解Java虚拟机》-----第12章 Java内存模型与线程

    概述 多任务处理在现代计算机操作系统中几乎已是一项必备的功能了.在许多情况下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,还有一个很重要的原因是计算机的运算速度与它的存储和通信子系统速 ...

随机推荐

  1. Android笔记--图像显示

    imageView 具体实现: 注意:图片名称应当为小写的英文字母与数字的结合,当然,二者可以只存在其一 图片的放置的比例:(通过scaleType属性设置) ImageButton ImageBut ...

  2. 【Keil】浅学一下keil中的.sct文件

    [Keil]浅学一下keil中的.sct文件 最近重新捣鼓了acfly的源码,有了新的有趣发现,当然,过程并不有趣. 起因 clone下来我去年提交的代码,编译 ...... 报错辣! linking ...

  3. fpm模式下读取到is_cli为何为true

    目录 问题出现和简单排查 排查 经过思考和猜测,严重怀疑是fpm读取到了cli下的opcache 原起 粗浅探索 测试代码 opcache配置 共享内存缓存与文件缓存 php-fpm下读取到is_cl ...

  4. 使用Net将HTML简历导出为PDF格式

    现在有许多将HTML导出PDF的第三方包,这里介绍使用的是Select.HtmlToPdf.NetCore 使用Select.HtmlToPdf.NetCore 整体思路是将cshtml内容读出来,然 ...

  5. 在一张 24 GB 的消费级显卡上用 RLHF 微调 20B LLMs

    我们很高兴正式发布 trl 与 peft 的集成,使任何人都可以更轻松地使用强化学习进行大型语言模型 (LLM) 微调!在这篇文章中,我们解释了为什么这是现有微调方法的有竞争力的替代方案. 请注意, ...

  6. hadoop 第二期

    Hive 启动hive 使用命令`hive` 输入命令之后要有一个 ; 结尾!!!!!!! DDL命令 1.创建数据表 create table lxl( num int, name string, ...

  7. Thinkpad T14升级Windows11ver22h2失败问题解决小记

    背景 手头的ThinkPad在近一年的时间里每次升级Windows 11的22h2版本每次都会报错,具体有以下几种情况: 更新过程中无问题,重启后黑屏更新过程中会卡在26%左右,然后蓝屏报KENERA ...

  8. Revit BIM模型在ArcGIS Pro中的数据组织及转换成SLPK后的图层结构解析

    ArcGIS Pro对Revit 数据有自己的一套分层方式. 在ArcGIS Pro中打开bim文件会发现都是按照相同的方式组织数据: 将rvt格式数据转换成SLPK格式后的数据结构(将slpk数据直 ...

  9. python之修改本地Ip地址

    安装模块pip install wmi # -*- coding: cp936 -*- # # FileName: ModifyIP.py # Date : 2008-01-15 # import w ...

  10. [Linux]Vmwaer创建CENTOS7虚拟机

    1 前期准备 CENTOS系统镜像 本实验版本: CENTOS 7.9 安装 VMware Workstation 本实验版本: VMware Workstation Pro 15.0 2 VMwar ...