深入理解JMM

一、什么是JMM

JMM(java memory model)Java内存模型:是java虚拟机规范中定义的一组规范,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让JAVA程序在各平台都能达到一致的并发结果。其主要规定了线程和内存之间的一些关系,并描述了和多线程相关的一组规范。它解决了CPU多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。

二、内存区域和内存模型的区分

  • java内存区域,也叫java内存、jvm内存模型,和java虚拟机(JVM)相关,java运行时将数据分区域存储,强调对内存空间的划分。(即:堆、栈、方法区、程序计数器等)
  • java内存模型,也叫内存模型(Jmm),是java定义的并发编程相关的一组规范,保证了操作的原子性、可见性、有序性。

原子性:指一个操作是不可分割的,在执行期间不能被中断。例如,对于基本数据类型的读写操作是原子的,而对于long和double类型的读写操作则可能不是原子的。
可见性:指当一个线程修改了共享变量的值后,其他线程可以立即看到这个变化。Java提供了volatile关键字来保证变量的可见性。
有序性:指程序执行的顺序必须与编写的代码顺序一致。Java中,使用synchronized关键字和Lock接口来保证代码块的原子性和有序性。

三、主内存和本地内存

JMM抽象了主内存和本地内存的概念。本地内存不是真的给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。

  • 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)

  • 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

3.1指令序列的重排

在执行程序的时候,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三类:

  • 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的测试和执行的顺序。
  • 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序:由于处理器使用缓存和读、写缓冲区,这使得加载和存储操作上看上去可能是乱序执行。

即系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,但是在在多线程下,指令重排序可能会导致一些问题。

3.2happens-before简介

从jdk5开始,java使用jsr-133内存模型,其用happens-before的概念来阐述操作之间内存的可见性。在JMM中,如果一个操作执行的结果需要对另一个操作结果可见,那么这两个操作之间就存在happens-before关系,这两个操作可以在同一个线程,也可以在两个线程内。

happens-before的规则:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁进行加锁。
  3. Volatile变量规则:对一个Volatile的写,happens-before于任意后续对这个Volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C ,即A happens-before C。

【两个操作之间具有happens-before关系,并不意味着,前一个操作必须要在后一个操作之前执行!仅仅需要前一个操作的结果对后一个操作的结果可见,且前一个操作顺序再后一个操作顺序之前即可】

针对上述话的解释,有如下图:

【未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致性的整体执行顺序。如上图所示:无论六个顺序如何改变,整体的执行顺序仍然按照A1-A2-A3,B1-B2-B3执行】

上述图片展示了happens-before于JMM的关系,对于程序员来说,happens-before 规则简单易懂,它避免程序员为了理解JMM提供的内存可见性而去学习复杂的重排序规则以及这些规则的具体实现方法。

四、内存模型的八种操作

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:

  • 读取(Read):从主内存中读取数据到工作内存。
  • 加载(Load):将读取的数据放入工作内存的变量副本中。
  • 使用(Use):执行代码,对变量进行操作,比如进行运算、赋值等。
  • 赋值(Assign):将工作内存中的数据写回主内存。
  • 存储(Store):将赋值的变量写入主内存。
  • 锁定(Lock):获取锁,标识进入同步代码块的线程独占资源。
  • 解锁(Unlock):释放锁,标识退出同步代码块,释放资源。
  • volatile读写:使用volatile关键字修饰的变量的读取和写入操作具有一定的内存屏障效果,可以保证可见性和有序性。

五、Volatile关键字详解

volatile 是 Java 并发编程的重要组成部分,它的主要作用有两个:保证内存的可见性和禁止指令重排序。Volatile关键字可以保证变量的可见性,如果将变量声明为volatile,这就指示JVM,这个变量是共享的且不稳定的,每次使用都需要到主存中读取,但不能保证数据的原子性。volatile写内存语义是执行刷新到主内存中,读的内存语义是直接从主内存中读取。

通过使用Java关键字synchronized、volatile和locks,可以实现Java中的同步。

在Java中,不能有同步变量。对变量使用synchronized关键字是非法的,会导致编译错误。您可以使用Java volatile变量代替Java中的同步变量,它将指示JVM线程从主内存读取volatile变量的值,而不是在本地缓存它。如果一个变量不在多个线程之间共享,那么就不需要使用volatile关键字。

public class Singleton {private static volatile Singleton _instance; // volatile variablepublic static Singleton getInstance() {if (_instance == null) {synchronized (Singleton.class) {if (_instance == null)_instance = new Singleton();}}return _instance;}
}

在第一个请求到来时创建实例:如果不将_instance变量设置为volatile,那么创建Singleton实例的线程将无法与其他线程通信。因此,如果线程A正在创建单例实例,只是在创建之后,CPU损坏等,所有其他线程将无法看到_instance的值不为空,他们会认为它仍然被分配为空。
为什么会发生这种情况?因为读线程没有做任何锁定,直到写线程从同步块中出来,内存不会被同步,_instance的值不会在主存中更新。在Java中使用Volatile关键字,这是由Java自己处理的,所有读取线程都可以看到这样的更新。

使用场景:

  • 当多个线程访问共享变量时,如果不使用volatile关键字,可能会出现可见性问题,即一个线程修改了变量的值,但其他线程无法感知到最新值的变化,导致数据不一致。
  • 当一个变量被多个线程频繁地修改和访问时,使用volatile关键字可以避免使用锁(synchronized)的开销,提高程序的性能。

六、Synchronized关键字详解

Synchronized是java的关键字,主要解决多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。早期,属于重量级锁,效率低下;Java 6后,synchronized引入了大量的优化如自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术减少锁操作的开销。

底层原理:synchronized同步语句块的实现使用的是 monitorenter和 monitorexit指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。

使用synchronized关键字的方式:

synchronized void method (){
//修饰实例方法
}
​
synchronized  static void method(){
//修饰静态方法
}
​
synchronized (this){
//修饰代码块
}

【重】synchronized关键字加到static静态方法和synchronized(class)代码块上都是给Class类加锁;synchronized关键字加到实例方法上是给对象加锁;尽量不要使用synchronized(string a)因为JVM中,字符串常量池具有缓存功能。

七、Volatile和synchronized区别

synchronized关键字和volatile关键字是两个互补的存在,而不是对立的存在!

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronize关键字要好 。但是volatile关键字只能用于变量而 synchronize关键字可以修饰方法以及代码块 。

  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

此篇文章大量参考《java并发编程的艺术》以及stackOverFlow:

java - What is the volatile keyword useful for? - Stack Overflow

 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【优选算法】专题1 -- 双指针 -- 移动零

前言: 📚为了提高算法思维,我会时常更新这个优选算法的系列,这个专题是关于双指针的练习 🎯个人主页:Dream_Chaser~-CSDN博客 一.移动零(easy) 描述: 「数组分两块」是⾮…

初识web自动化测试,快速成长指南

自动化 说明 让机器设备代替人为完成指定目标的而过程 优点 减少劳动力提高效率(批量生产)提高产品质量规格统一标准 自动化测试 概念 : 让程序代替人工去验证系统功能的过程 自动化测试能解决什么问题? 解决-回归测试 [重点]解决-压力测试解决-兼容性测试 …

Ubuntu 14.04:安装PaddlePaddle(Conda安装)

目录 一、PaddlePaddle 概要 二、PaddlePaddle安装要求 三、PaddlePaddle安装 3.1 安装 Anaconda3 3.2 创建Anaconda虚拟环境(python 3.8) 3.3 进入Anaconda虚拟环境 3.4 检测 Anaconda 虚拟环境配置是否符合PaddlePaddle安装要求 3.4.1 确认 py…

掘根宝典之C++类型别名,关键字typedef,auto,decltype

类型别名 在C中,我们可以使用typedef关键字或using关键字来创建类型别名。下面是两种方式的示例: 使用typedef关键字创建类型别名: typedef int myInt; typedef float myFloat;myInt a;//等价int a; myFloat b;//等价float b; 使用using关…

Python面向对象构造函数:手把手教你如何玩转对象初始化

我们都知道,Python是一个面向对象的语言,这意味着我们可以用类来定义对象的属性和方法。而构造函数,就是当我们创建一个新的对象时,会自动调用的特殊方法。那么,如何玩转这个构造函数呢? 首先,…

YoloV8改进策略:下采样改进|HWD改进下采样

摘要 本文使用HWD改进下采样,在YoloV8的测试中实现涨点。 论文解读 在卷积神经网络(CNNs)中,极大池化或跨行卷积等下采样操作被广泛用于聚合局部特征、扩大感受野和最小化计算开销。然而,对于语义分割任务&#xff…

golang中new和make的区别

1. 先看一个例子 package mainimport "fmt"func main() {var a *int*a 10fmt.Println(*a) }运行结果是啥呢? 问:为什么会报这个panic呢? 答:因为如果是一个引用类型,我们不仅要声明它,还要为…

MySQL 压测与结果分析

文章目录 说明1. 安装部署1.1 二进制包1.2 源码包 2. 服务器性能测试2.1 CPU2.2 内存2.3 磁盘 3. MySQL 基准测试3.1 参数解析3.2 压测命令3.3 输出解读3.4 结果分析 说明 Sysbench 是一个开源的多线程基准测试工具,也是目前使用最多的 MySQL 压力测试工具。本篇文…

JVM是如何运行的

JVM(Java Virtual Machine,Java虚拟机)是 Java 程序的运行环境,它负责将 Java 字节码翻译成机器代码并执行。也就是说 Java 代码之所以能够运行,主要是依靠 JVM 来实现的。 JVM 整体的大概执行流程是这样的&#xff1…

Android cmdline tools安装

打开AS 进入SDK Tools 看到了吗?那个打着勾的就是

Centos8安装Docker,使用阿里云源

一、前期准备 1.关闭防火墙,SELINUX systemctl stop firewalld.service systemctl disable firewalld.service setenforce 0 sed -i "s/SELINUXenforcing/SELINUXdisabled/g" /etc/selinux/config查看状态 systemctl status firewalld systemctl status…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:NavRouter)

导航组件,默认提供点击响应处理,不需要开发者自定义点击事件逻辑。 说明: 该组件从API Version 9开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。 子组件 必须包含两个子组件,其中第二个子组…

c++入门你需要知道的知识点(上)

🪐🪐🪐欢迎来到程序员餐厅💫💫💫 今日主菜:c入门 主厨:邪王真眼 所属专栏:c专栏 主厨的主页:Chef‘s blog 前言: 咱也是好久没有更…

大数据与云计算

目录 一、大数据时代二、云计算——大数据的计算三、云计算发展现状四、云计算实现机制五、云计算压倒性的成本优势 一、大数据时代 我们先来看看百度关于 “大数据”(Big Data)的搜索指数。 可以看出,“大数据” 这个词是从2012年才引起关注…

flask-sqlalchemy库

彩笔激流勇退。 1. 简介 ORM,对象关系映射。简单来说,ORM将数据库中的表与面向对象中的类建立了一种对应关系。这样,我们要操作数据库,表,记录就可以直接通过操作类或者类实例来完成。 SQLAlchemy 是目前python中最…

面向对象编程第二式:继承 (Java篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. 🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人…

【Golang】golang使用三方SDK操作容器指南

【Golang】golang使用三方SDK操作容器指南 大家好 我是寸铁👊 总结了一篇 golang使用三方SDK操作容器✨ 喜欢的小伙伴可以点点关注 💝 这应该是目前全网最全golang使用三方SDK操作容器的指南了✌️ CreateConfig 主要是创建容器的配置信息,常…

uniapp遇到的问题

【uniapp】小程序中input输入框的placeholder-class不生效解决办法 解决:写在scope外面 uniapp设置底部导航 引用:https://www.jianshu.com/p/738dd51a0162 【微信小程序】moveable-view / moveable-area的使用 https://blog.csdn.net/qq_36901092/…

【机器学习】走进监督学习:构建智能预测模型的第一步

🎈个人主页:豌豆射手^ 🎉欢迎 👍点赞✍评论⭐收藏 🤗收录专栏:机器学习 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进…

go语言基础笔记

1.基本类型 1.1. 基本类型 bool int: int8, int16, int32(rune), int64 uint: uint8(byte), uint16, uint32, uint64 float32, float64 string 复数:complex64, complex128 复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部…