java内存模型的理解

news2024/12/26 10:48:15

java内存模型的理解

  • 并发问题产生的源头
    • 缓存导致的可见性问题
    • 线程切换导致的原子性问题
    • 编译优化带来的有序性问题
    • 小结
  • Java内存模型: 解决可见性和有序性问题
    • Java内存模型与JVM内存模型的区别
    • volatile关键字
    • Happens-Before规则
    • 小结
    • 思考题
  • 参考


并发问题产生的源头

缓存导致的可见性问题

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

在单核CPU时代,所有线程都是被同一个CPU调度,因此共享同一块cpu的缓存,不存在缓存一致性问题:
在这里插入图片描述
多核CPU时代,每个线程都有可能同时被不同的cpu调度,每个cpu都有各自的缓存,并且读写优先走缓存,这就会导致缓存一致性问题:

在这里插入图片描述
高速缓存和主内存之间如何保持数据一致性


线程切换导致的原子性问题

CPU能保证的原子操作是CPU指令级别的,高级语言里一条语句往往需要多条 CPU 指令完成,例如:count += 1,至少需要三条 CPU 指令:
在这里插入图片描述


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

有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,从而导致意外的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创建对象在java中分为三步:

  • 分配一块内存 M;
  • 在内存 M 上初始化 Singleton 对象;
  • 然后 M 的地址赋值给 instance 变量。

但是如果编译器进行了指令重排序优化,变成了下面这样:

  • 分配一块内存 M;
  • 将 M 的地址赋值给 instance 变量;
  • 最后在内存 M 上初始化 Singleton 对象。

在下面场景中,线程B访问未初始化过的instance,可能会触发空指针异常:
在这里插入图片描述


小结

java并发编程问题三大根本来源: 可见性,原子性,有序性

  • 缓存导致的可见性问题
  • 线程切换导致的原子性问题
  • 编译优化导致的有序性问题

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

Java内存模型与JVM内存模型的区别

  1. Java内存模型定义了一套规范,能使JVM按需禁用cpu缓存和禁止编译优化。这套规范包括对volatile, synchronized, final三个关键字的解析,和7个Happen-Before规则。

  2. JVM内存模型是指程序计数器,虚拟机栈,本地方法栈,堆,方法区这5和要素。


volatile关键字

volatile在c语言中最原始的含义就是禁用cpu缓存,volatile修饰符表达的是: 对某个变量的读写,不能使用cpu缓存,必须从内存中读取或者写入。

大家看下面这个例子: 线程A执行writer方法,按照volatile语义,会把变量v=true写入内存,假设线程B执行reader方法,同样按照volatile语义,线程B会从内存中读取变量v, 如果线程B看到v==true时,那么线程B看到的变量x的值是多少呢?

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}

答案:

  • jdk 1.5之前,x可能是42,也可能是0 ,因为变量x可能被cpu缓存而导致可见性问题
  • jdk 1.5之后, x就是等于42。因为java内存模型在1.5版本对volatile语义进行了增强

怎么增强的呢?

  • happens-before规则

Happens-Before规则

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

Happens-Before规则约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守Happens-Before规则。

具体规则如下:

  1. 程序的顺序性规则:在一个线程中,前面的操作Happens-Before于后续的任意操作。
  2. volatile变量规则:对一个volatile变量的写操作Happens-Before于对这个volatile变量的读操作。
  3. 传递性规则:A Happens-Before B,B Happens-Before C,那么A Happens-Before C。
    在这里插入图片描述

从图中,我们可以看到:

  • “x=42” Happens-Before 写变量 “v=true” ,这是规则 1 的内容;
  • 写变量“v=true” Happens-Before 读变量 “v=true”,这是规则 2 的内容 。

再根据这个传递性规则,我们得到结果:“x=42” Happens-Before 读变量“v=true”。这意味着什么呢?如果线程 B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 是可见的。也就是说,线程 B 能看到 “x == 42” ,有没有一种恍然大悟的感觉?这就是 1.5 版本对 volatile 语义的增强,这个增强意义重大,1.5 版本的并发工具包(java.util.concurrent)就是靠 volatile 语义来搞定可见性的。

  1. 管程中锁的规则:synchronized是Java对管程的实现,隐式加锁、释放锁,对一个锁的解锁Happens-Before于后续对这个锁的加锁。
  2. 线程start()规则:主线程A启动子线程B后,start()操作 Happens-Before于子操作中的任意操作。
  3. 线程join规则:在线程A中调用线程B的join()并返回,线程B中的任意操作 Happens-Before于join()的返回。
  4. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  5. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

上述很多规则都需要配合传递性规则进行理解。


小结

Java内存模型涉及的几个关键词:锁、volatile字段、final修饰符与对象的安全发布。

  • 第一是锁,锁操作是具备happens-before关系的,解锁操作happens-before之后对同一把锁的加锁操作。实际上,在解锁的时候,JVM需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。
  • 第二是volatile字段,volatile字段可以看成是一种不保证原子性的同步但保证可见性的特性,其性能往往是优于锁操作的。但是,频繁地访问 volatile字段也会出现因为不断地强制刷新缓存而影响程序的性能的问题。
  • 第三是final修饰符,final修饰的实例字段则是涉及到新建对象的发布问题。当一个对象包含final修饰的实例字段时,其他线程能够看到已经初始化的final实例字段,这是安全的。

Java内存模型底层怎么实现的?

  • 主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。

在java中,Happens-Before规则本质还是一种可见性,A Happens-Before B,意味着A事件对B事件来说是可见的,无论A事件和B事件是否发生在同一个线程里,例如: 事件A发生在线程1,事件B发生在线程2,Happens-Before规则保证线程2上也能看到A事件的发送。


思考题

还是文中给出的案例,大家思考会不会产生x=42和v=true重排序,导致线程B读取到x=0的结果呢?

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  
  public void writer() {
    x = 42;
    v = true;
  }
  
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
  
}

解答:

  • 程序顺序性规则是针对单线程的。如果只考虑单线程,那么编译器可以对范例代码进行指令重排优化。但是对于多线程,volatile 变量规则、传递性这 2 条规则,就附加了新的限制。对于多线程,这 3 条 happens-before 规则要求,线程 B 在读到 v = true 的时候,也能见到 x = 42。如果编译器仍然按照单线程的情况,对这两条语句进行指令重排,把 v = true 放到 x = 42 之前。那么,线程 B 就有可能看不到 x 的值为 42。这显然违背了 happens-before 的规定。编译器为了符合规则,只能不进行指令重排优化了。
  • 为了符合 happens-before 规定,对于示例代码,编译器不能进行指令重排的编译优化。但实际上,仅仅不进行指令重排编译优化,并不能保证编译后的代码的执行结果,符合 happnes-before 规定。因为指令重排这一优化措施,并不仅仅是编译器会做。现代 cpu 在执行机器指令的时候,同样会做指令重排的优化。所以,如果 cpu 在执行机器指令时,发生了机器指令的重排序。上述实例代码的结果,仍然有机会不符合 happens-before 规定。
  • 为了令到编译后的代码的执行结果,能够符合 happens-before 规定。编译器除了不能做指令重排序编译优化之外,还要在生成的机器代码中。加入特定 cpu 指令,令到 cpu 只会执行完这条特定指令之后,才会执行后续的其它机器指令。而这种特定指令,是通过建立 “内存屏障” 来禁止 cpu 的指令重排序的。那为什么叫 “内存屏障” 呢?可以理解为一种特殊指令,要求 cpu 把缓存数据写回到主内存中。这就像在内存中建立了一道屏障,令到后面的代码不能越过屏障,提前执行。

jmm 是一个规范,它用于指导编译器的行为。但它本身不会限制编译器所使用的具体编译技术。所以,在 jmm 规范中,不会提到 “指令重排” 或者 "内存屏障” 这些具体的实现技术。这是我们在学习规范类知识的时候,需要注意的。


参考

JAVA并发编程实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/389888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Verilog】——Verilog简介

目录 1.简介 2.什么是HDL以及HDL的功能 3.Verilog和C语言的比较 4.Verilog的用途 5.数字系统的抽象层次 1.系统级 2.算法级 3.RTL级(寄存器变换级) 6.数字系统抽象层级 7.自顶向下的结构化设计方法 8.Verilog建模 9.Verilog概述 10.Verilog模块的基本…

Django学习17 -- ManytoManyField

1. ManyToManyField (参考:Django Documentation Release 4.1.4) 类定义 class ManyToManyField(to, **options)使用说明 A many-to-many relationship. Requires a positional argument: the class to which the model is related, which w…

推导部分和——带权并查集

题解: 带权并查集 引言: 带权并查集是一种进阶的并查集,通常,结点i的权值等于结点i到根节点的距离,对于带权并查集,有两种操作需要掌握——Merge与Find,涉及到路径压缩与维护权值等技巧。 带…

用Python批量重命名文件

案例 今天,我们来整理文件夹中的文件,给某个文件夹下的所有文件重新命名。要求是给所有文件按照修改时间,按顺序在文件名前面加上编号。比如将文件资料.xlsx重命名为1. 资料.xlsx import osdef Get_modify_time(file):return os.path.getmtime(file) #获取文件修改时间path…

vue3的v-model指令

1. 普通input输入框双向绑定 <template><!-- 1. 普通input输入框双向绑定 --><!-- 其实等价于&#xff1a;<input :modelValue"title" update:modelValue"newTitle>titlenewTitle"/> --><input type"text" v-mod…

Junit测试框架

一、简介 Junit框架是一个开源的Java语言单元测试框架&#xff0c;Java方向使用最广泛的单元测试框架&#xff0c;使用Java开发者都应该学习Junit并能掌握单元测试的编写。 对于Junit和Selenium的关系&#xff1a;通俗点来说Selenium如果比喻为灯泡&#xff0c;那么Junit就是电…

【蓝桥杯集训15】求最短路存在负权边——spaf算法(2 / 4)

——SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称 单源最短路&#xff0c;且图中没有负环就可以用spfa 目录 spaf求最短路模板 852. spfa判断负环 341. 最优贸易 - 3305. 作物杂交 - spaf求最短路模板 只有当一个点的前驱结点更新了&#xff0c;该节点才会得到…

操作系统——16.时间片轮转、优先级、多级反馈队列算法

这篇文章我们来看一下进程调度算法中的时间片轮转、优先级、多级反馈队列算法 目录 1.概述 2.时间片轮转调度算法&#xff08;RR&#xff0c;Round-Robin&#xff09; 3.优先级调度算法 4.多级反馈队列调度算法 5.分析对比 1.概述 首先&#xff0c;我们来看一下这篇文章…

计算机网络整理

TCP与UDP 介绍 HTTP&#xff1a;&#xff08;HyperText Transport Protocol&#xff09;是超文本传输协议的缩写&#xff0c;它用于传送WWW方式的数据&#xff0c;关于HTTP协议的详细内容请参考RFC2616。HTTP协议采用了请求/响应模型。 TCP:&#xff08;Transmission Contro…

[YOLO] yolov3、yolov4、yolov5改进

yolov3网络结构图&#xff1a; Yolov3的三个基本组件&#xff1a; &#xff08;1&#xff09;CBL&#xff1a;Yolov3网络结构中的最小组件&#xff0c;由ConvBnLeaky_relu激活函数三者组成。 &#xff08;2&#xff09;Res unit&#xff1a;借鉴Resnet网络中的残差结构&#x…

docker 入门篇

docker为什么会出现&#xff1f; 一款产品&#xff1a;开发---->运维&#xff0c;两套环境&#xff01;应用环境&#xff0c;应用配置&#xff01; 常见问题&#xff1a;我的电脑可以运行&#xff0c;版本更新&#xff0c;导致服务不可用。 环境配置十分的麻烦&#xff0c;…

RL笔记:基于策略迭代求CliffWaking-v0最优解(python实现)

目录 1. 概要 2. 实现 3. 运行结果 1. 概要 CliffWalking-v0是gym库中的一个例子[1]&#xff0c;是从Sutton-RLbook-2020的Example6.6改编而来。不过本文不是关于gym中的CliffWalking-v0如何玩的&#xff0c;而是关于基于策略迭代求该问题最优解的实现例。 CliffWalking-v0的…

Promise-异步回调

1.理解Promise promise是ES6提出的异步编程的新的解决方案&#xff0c;通过链式调用解决ajax回调地狱 从语法上看&#xff0c;promise是一个构造函数&#xff0c;自己身上有all、reject、resolve方法&#xff0c;原型上有then、catch方法 从功能上看&#xff0c;Promise对象用…

BloomFilter原理学习

文章目录BloomFilter简单介绍BloomFilter中的数学知识fpp(误判率/假阳性)的计算k的最小值公式总结编程语言实现golang的实现[已知n, p求m和k](https://github.com/bits-and-blooms/bloom/blob/master/bloom.go#L133)参考BloomFilter简单介绍 BloomFilter我们可能经常听到也在使…

瑞吉外卖——day2

目录 一、新增员工 二、查询分页数据 三、启用、禁用员工账户、编辑员工信息 一、新增员工 点击左上角新增员工 页面如下&#xff1a; 我们随便填数据 &#xff0c;点击保存&#xff0c;请求的地址如下 返回前端可以看到请求方式为Post 在employeeController中编写对应的代…

Elasticsearch:图片相似度搜索的 5 个技术组成部分

作者&#xff1a;Radovan Ondas&#xff0c;Bernhard Suhm 在本系列博文的第一部分中&#xff0c;我们介绍了图像相似度搜索&#xff0c;并回顾了一种可以降低复杂性并便于实施的高级架构。 此博客解释了实现图像相似性搜索应用程序所需的每个组件的基本概念和技术注意事项。 学…

Python采集本地二手房,一键知晓上万房源信息

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 所以今天教大家用Python来采集本地房源数据&#xff0c;帮助大家筛选好房。 话不多说&#xff0c;让我们开始愉快的旅程吧~ 更多精彩内容、资源皆可点击文章下方名片获取此处跳转 本文涉及知识点 采集基本流程 requests 发送…

【Java】Spring Boot整合WebSocket

【Java】Spring Boot整合WebSocket WebSocket简介 WebSocket是一种协议&#xff0c;用于实现客户端和服务器之间的双向通信。它可以在单个TCP连接上提供全双工通信&#xff0c;避免了HTTP协议中的请求-响应模式&#xff0c;从而实现更高效的数据交换。WebSocket协议最初由HTM…

【计算几何】贝塞尔曲线 B样条曲线简介及其离散化 + Python C++ 代码实现

文章目录一、贝塞尔曲线二、B样条曲线三、Python 代码实现B样条曲线离散化四、C 代码实现B样条曲线离散化4.1 主要代码4.2 其余类4.3 离散效果展示&#xff08;在CAD中展示&#xff09;本文只做简介&#xff0c;关于贝塞尔曲线和B样条曲线的详细介绍&#xff0c;请参考&#xf…

unity UGUI系统梳理 - 基本布局

偷懒了&#xff0c;部分节选unity API API 1、矩形工具 为了便于布局&#xff0c;每个 UI 元素都表示为矩形。可使用工具栏中的__矩形工具 (Rect Tool)__ 在 Scene 视图中操纵此矩形。矩形工具既可用于 Unity 的 2D 功能&#xff0c;也可用于 UI&#xff0c;实际上甚至还可用…