浅析Java内存模型

概述

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量是线程共享的,存在竞争问题的。

 

Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主内存中的变量。(注:这里说的工作内存或本地内存都是虚拟出来的,实质上包括了寄存器、缓存或中间的存储器)

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,三者关系的交互如图所示:

如何交互

那么内存之间是如何交互的呢?JMM定义了8中操作来完成,虚拟机保证每一种操作都是原子性的。

Lock:作用于主内存的变量,它把一个变量标识为线程独占状态

Unlock:作用于主内存的变量,将一个处于锁定状态下的变量释放出来,释放后的变量才可以被其他线程加锁。

Read:作用于主内存的变量,将一个变量的值从主内存传输到线程的工作内存。

Load:作用于工作内存的变量,将read操作得到的变量的值放入工作内存中的变量副本中

Use:作用于工作内存的变量,将工作内存中变量的值传递给执行引擎,当虚拟机需要使用时会执行这个操作。

Assign:作用于工作内存的变量,将一个执行引擎接收到的值赋给工作内存中的变量,当虚拟机遇到给变量赋值的字节码时会执行此操作。

Store:将工作内存中的一个变量值传送到主内存中

Write:作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

 

重排序

从上面可以大致看出来线程之间通信的流程,但是,在执行程序时,编译器和处理器往往会对指令进行重排序,也就是不一定按照程序写的执行。这往往是为了提高性能,提高并发度。但是对于多线程程序来说,这往往会造成程序执行的结果不一致,所以我们就得需要通过synchronized,volatile等方式进行同步。

内存屏障

由于有重排序,为了保证内存的可见性,java编译器在生成的指令序列中会插入内存屏障指令来禁止特定类型的处理器重排序,JMM把内存屏障指令分为下列四类:

屏障类型

指令示例

说明

LoadLoad Barriers

Load1;

LoadLoad;

Load2

确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。

StoreStore Barriers

Store1;

StoreStore;

Store2

确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。

LoadStore Barriers

Load1;

LoadStore;

Store2

确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。

StoreLoad Barriers

Store1;

StoreLoad;

Load2

确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。

StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

 

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并不是说,两个操作代码上执行时间的先后顺序,而是保证一个操作的结果对另一个操作可见,保证执行结果是顺序的。

 

 

数据依赖性

 

如果两个操作访问同一个变量,且其中两个操作有一个是写操作,此时这两个操作之间就存在数据依赖性。

数据依赖分为三种类型:

 

写后读:

a=1; b=a;

写后写:

a=1; a=1;

读后写:

b=a; a=1;

所谓的数据依赖性就是指当发生重排序的时候,其结果会发生改变,所以编译器和处理器在重排序的时候,不会改变存在数据依赖性的两个操作的执行顺序

 

 

As-if-serial语义:

 

不管怎么重排序,(单线程)程序的执行结果不能被改变。所以为了遵循这种语义,编译器和处理器不会对存在数据依赖性的操作进行重排序。

 

 

控制依赖性:

 

if(flag) //---1

int i= a*a; //-----2

操作1和操作2之间存在控制依赖,所以在多线程的程序中,编译器和处理器会启动猜测执行

处理器可以提前计算a*a的结果,然后放到一个重排序的缓冲的硬件缓存中,当操作1的条件为真时,将计算结果写入到变量i 中。所以我们会发现在这里对两个操作做了重排序,所以破坏了多线程程序的语义。

但是对于单线程而言,重排序存在控制依赖的操作,不会改变执行结果,但是在多线程中,重排序存在控制依赖的操作,可能会改变执行结果。

 

顺序一致性:

 

当程序未使用同步时,就会出现数据竞争,所以会造成结果的改变。

当使用了同步以后,这便是一个没有数据竞争的程序。如果程序是正确使用同步的,那么执行的程序将会具有顺序一致性:即程序的执行结果与在顺序一致性模型中执行的完全相同。

 

所谓顺序一致性模型是一个理论的参考模型,具有两大特性:

一个线程中的所有操作都必须按照程序的顺序来执行;

无论程序是否同步,所有线程都只能看到一个单一执行顺序,在顺序一致性模型中,每个操作都是原子性的执行而且必须立即对其他线程可见。

 

注:

JMM不保证对64位的long和double型的变量(没有volatile修饰)读写具有原子性,而内存一致性模型保证对所有的读写操作都具有原子性。

 

因为在一些32位的处理器上,如果对64位的long和double读写具有原子性,那么需要很大的开销,所以java不强求必须对这两种具有原子性。JVM在这些处理器上运行时,会将一个64位的long/double写操作分成两个32位的写操作来执行,此时对其就无法保证原子性,所以有可能造成读取的时候读了一半数的错误。

volatile

volatile可是看成是弱一级的Synchronized,换句话说就是给volatile变量单个的读写操作,使用同一个锁对这些单个的操作进行了同步。

我们来看一下volatile的效果:

使用volatile:

[java] view plaincopy

  1. class VolatileFeaturesExample {

  2. //使用volatile声明64位的long型变量

  3. volatile long vl = 0L;

  4.  

  5. public void set(long l) {

  6. vl = l; //单个volatile变量的写

  7. }

  8.  

  9. public void getAndIncrement () {

  10. vl++; //复合(多个)volatile变量的读/写

  11. }

  12.  

  13. public long get() {

  14. return vl; //单个volatile变量的读

  15. }

  16. }

使用synchronized替换volatile

[java] view plaincopy

  1. class VolatileFeaturesExample {

  2. long vl = 0L; // 64位的long型普通变量

  3.  

  4. //对单个的普通 变量的写用同一个锁同步

  5. public synchronized void set(long l) {

  6. vl = l;

  7. }

  8.  

  9. public void getAndIncrement () { //普通方法调用

  10. long temp = get(); //调用已同步的读方法

  11. temp += 1L; //普通写操作

  12. set(temp); //调用已同步的写方法

  13. }

  14. public synchronized long get() {

  15. //对单个的普通变量的读用同一个锁同步

  16. return vl;

  17. }

  18. }

 

我们可以看出来,对Volatile的单个读写操作,与对一个普通变量的读写操作使用同一个锁来同步,之间的效果是一样的。

我们发现及时64位的long和double,只要是volatile,那么该变量的读写就是原子性的,而volatile++这时复合操作,所以不具有原子性。

volatile的性质(可见性和原子性)

可见性:读取一个volatile变量的时候,总是可以看到其他线程对这个变量的最后写入,也就是说每次读取的都是最新值。

原子性:对任意单个volatile变量操作具有原子性

 

我们可以从可见性得到,volatile可以建立一定意义上的happens-before关系,因为其写优先于读。

执行流程

当线程A写一个volatile变量的时候,JMM会把该变量本地内存中的值刷新到主内存,因此本地内存中的值和主内存中的是一致的。

 

当线程B读取一个volatile,JMM会将该线程的本地内存置为无效,直接从主内存中读取,这样读取到的值就是刚刚写入的

 

也就是说线程B在读取volatile变量的时候,线程A之前所有对此变量的操作都对线程B可见

 

换个角度,我们可以这么说:

线程A在写入一个volatile变量,实际上是对下一个将要读取这个volatile变量的线程B发出了消息,而线程B读取这个volatile变量就是接收了线程A发出的消息。

所以线程A写volatile,线程B读volatile,可以看成线程A通过主内存向线程B发送了消息。

如何实现

我们知道重排序,那么是否会对volatile变量重排序呢,JMM限制了这种变量的重排序规则:

 

是否能重排序

第二个操作

第一个操作

普通读/写

volatile读

volatile写

普通读/写

  

NO

volatile读

NO

NO

NO

volatile写

 

NO

NO

 

从上表中我们可以看出:

如果第二个操作是volatile的写,无论第一个操作是什么,都不会被编译器或处理器重排序。确保volatile之前的写操作不会被排到其后面。

如果第一个操作是volatile的读,无论第二个操作是什么,都不会被重排序。

如果第一个操作是volatile写,而第二个是volatile读,不能重排序。

 

volatile为了实现这个规则,编译器在生成字节码的时候,给字节码指令前后都添加了内存屏障,禁止特定类型的处理器重排序。

规则如下:

给每个volatile的写操作前面添加了StoreStore屏障

===》禁止之前的普通写操作与volatile写操作进行重排序

 

给每个volatile的写操作后面添加了StoreLoad屏障

===》禁止后面的普通读/写操作与volatile写操作重排序

 

给每个volatile的读操作后面添加了LoadLoad屏障

===》防止后面的普通读操作和volatile读操作的进行重排序

给每个volatile的读操作后面添加了LoadStore屏障

===》防止后面的写操作与volatile读操作进行重排序

 

 

在实际情况下可以省略一些屏障,而且屏障的设立和处理器也有很大关系,像X86仅仅会有StoreLoad屏障,因为它只允许写-读操作的重排序。

 

 

上文说到了锁可以保证强大的互斥性和同步性

 

锁同样和volatile一样,也可以建立happens-before关系

 

比如说线程A只有释放锁了以后,线程B可以获得锁,因此线程释放锁之前对共享变量的修改,在线程B获得锁之后都是可见的。

 

锁与volatile

Volatile仅仅只对单个volatile变量的读写操作具有原子性,而锁很强大,可以保证整个临界区代码执行都具有原子性,所以相对而言volatile的可伸缩性和性能比较好。

 

内存语义

线程A释放锁的时候,JMM会把线程对应的本地内存中的共享变量刷新到主内存中。

当线程B获取锁时,JMM会把线程对应的本地内存置为无效,直接从主内存中读取共享变量。

 

类似于volatile,锁也有自己的语义,线程A释放锁的时候,其实就是给将要获取这个锁的线程发出了消息

线程B获得锁也就是接收了线程A所发送的消息

 

这个过程其实就是两个线程通过主内存进行通信

锁的实现

通常是通过ReentrantLock 中的lock方法实现,这个锁一般分为公平锁和非公平锁,默认为非公平锁。

 

公平锁在释放锁的最后写volatile变量state,在获取锁的时候首先读取volatile变量,所以根据volatile的happens-before规则,释放锁的线程写入的volatile对获取锁的线程可见。

 

非公平锁会调用compareAndSetState(CAS),这个方法最终调到本地方法上,一些内在的规定使得CAS同时具有volatile读和volatile写的内存语义。

 

公平锁和非公平锁:

公平锁和非公平锁在释放时,最后都要写一个volatile变量state

公平锁在获取时首先读取这个volatile

而非公平锁在获取时,首先会用CAS更新这个volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

 

final

对于final域的读写,编译器和处理器需要遵循两个重排序规则:

1、在构造函数中对final域的写入,与随后将此被构对象的引用赋给其他引用,两者之间不能重排序。

2、初次读取一个包含final域的引用和随后读取这个final域,两者之间不能重排序

 

 

写final域的重排序规则禁止把final域的写重排序到构造函数之外

编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障,这个屏障禁止final域的写重排序到构造函数外面,

这样可以保证其他任何线程,在引用对象时,对象的final域已经正确初始化(但是普通域有可能没有被初始化)。

 

 

编译器会在读final域操作的前面插入一个loadLoad屏障。

这样可以确保,在读取一个对象的final域之前,一定会先读取包含这个final域的对象引用,如果该引用不为null,说明引用对象的final域已经被正确初始化过了

 

 

如果final域是一个引用类型,那么会增加一个约束:

在构造函数内对final引用对象的成员域的写入,和在构造函数外把这个被构造对象的引用赋值给另一个引用变量,两者之间不能重排序。

 

写final域的重排序规则可以确保,在引用变量对其他线程可见之前,该引用变量指向的对象已经在构造函数中被正确初始化过了但是还要有一个保证就是:在构造函数返回之前,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能再构造函数中溢出。

 

我们看个例子:

[java] view plaincopy

  1. public class FinalReferenceEscapeExample {

  2. final int i;

  3. static FinalReferenceEscapeExample obj;

  4.  

  5. public FinalReferenceEscapeExample () {

  6. i = 1; //1写final域

  7. obj = this; //2 this引用在此“逸出”

  8. }

  9.  

  10. public static void writer() {

  11. new FinalReferenceEscapeExample ();

  12. }

  13.  

  14. public static void reader {

  15. if (obj != null) { //3

  16. int temp = obj.i; //4

  17. }

  18. }

  19. }

线程A执行write方法,线程B执行read方法。

 

操作2使得对象在未完成构造前就对线程B可见,所以这有可能使得操作2和操作1重排序,然后线程B就无法正确读取到final域的值

final的实现

上面已经说过,再来总结一下:

写final域的重排序规则要求编译器在 final域的写之后,构造函数return之前插入StroeStore屏障,

读final域的重排序规则要求编译器在读final域的操作前面插入一个loadLoad屏障

总结

JMM把happens-before要求禁止的重排序分成了两种:

对于会改变程序执行结果的重排序,JMM要求编译器和处理器会禁止这种排序

对于不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(可以重排序)

 

也可以说,JMM遵循一个原则:

只要不改变程序(单线程程序和多线程正确同步的程序)的执行结果,编译器和处理器怎么优化都可以。

 

转载于:https://www.cnblogs.com/salansun/p/4746819.html

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

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

相关文章

fastapi quickstart学习

文章目录1. 安装包2. 编写代码3. 终端运行4. 文档5. 增加数据learn from https://fastapi.tiangolo.com/zh/#typer-fastapi 1. 安装包 # pip install fastapi # pip install uvicorn[standard]2. 编写代码 main.py from typing import Optional # typing 模块用于类型检查…

单列变双列css_css 两列布局中单列定宽单列自适应布局的6种思路

前面的话说起自适应布局方式,单列定宽单列自适应布局是最基本的布局形式。本文将从float、inline-block、table、absolute、flex和grid这六种思路来详细说明如何巧妙地实现布局float【思路一】float说起两列布局,最常见的就是使用float来实现。float浮动…

fastapi 用户指南(路径参数、查询参数、请求体)

文章目录1. 第一步1.1 小结2. 路径参数2.1 顺序很重要2.2 预设值2.3 包含路径的路径参数3. 查询参数3.1 查询参数类型转换4. 请求体learn from https://fastapi.tiangolo.com/zh/tutorial/1. 第一步 pip install fastapi[all] from fastapi import FastAPI my_app FastAPI(…

DirectX API 编程起步 #01 项目设置

目录: DirectX API 编程起步 #02 窗口的诞生 DirectX API 编程起步 #02 创建3D世界 这里记录了我从零开始学习使用 DirectX API 的过程。 参考教程:rastertek -> DirectX 11 Tutorials 准备工作 1)首先我们要有一套能写代码的软件&#…

LeetCode 2016. 增量元素之间的最大差值

文章目录1. 题目2. 解题1. 题目 给你一个下标从 0 开始的整数数组 nums &#xff0c;该数组的大小为 n &#xff0c;请你计算 nums[j] - nums[i] 能求得的 最大差值 &#xff0c;其中 0 < i < j < n 且 nums[i] < nums[j] 。 返回 最大差值 。如果不存在满足要求…

LeetCode 2017. 网格游戏(前缀和)

文章目录1. 题目2. 解题1. 题目 给你一个下标从 0 开始的二维数组 grid &#xff0c;数组大小为 2 x n &#xff0c;其中 grid[r][c] 表示矩阵中 (r, c) 位置上的点数。 现在有两个机器人正在矩阵上参与一场游戏。 两个机器人初始位置都是 (0, 0) &#xff0c;目标位置是 (1,…

zabbix设置mysql登陆免报警_zabbix3.0 监控mysql服务免用户名密码登录的有关问题故障处理详细过程_mysql...

zabbix3.0 监控mysql服务免用户名密码登录的问题故障处理详细过程1&#xff0c;My.cnf中用户名密码无效在azure云上面&#xff0c;使用Zabbix监控mysql中&#xff0c;发现在/usr/local/mysql/my.cnf里面设置的默认用户名密码无效&#xff0c;出不来数据&#xff0c;而且在zabbi…

LeetCode 2018. 判断单词是否能放入填字游戏内(模拟)

文章目录1. 题目2. 解题1. 题目 给你一个 m x n 的矩阵 board &#xff0c;它代表一个填字游戏 当前 的状态。 填字游戏格子中包含小写英文字母&#xff08;已填入的单词&#xff09;&#xff0c;表示 空格 的 和表示 障碍 格子的 # 。 如果满足以下条件&#xff0c;那么我…

LeetCode 2022. 将一维数组转变成二维数组

文章目录1. 题目2. 解题1. 题目 给你一个下标从 0 开始的一维整数数组 original 和两个整数 m 和 n 。 你需要使用 original 中 所有 元素创建一个 m 行 n 列的二维数组。 original 中下标从 0 到 n - 1 &#xff08;都 包含 &#xff09;的元素构成二维数组的第一行&#xf…

LeetCode 2023. 连接后等于目标字符串的字符串对

文章目录1. 题目2. 解题1. 题目 给你一个 数字 字符串数组 nums 和一个 数字 字符串 target &#xff0c;请你返回 nums[i] nums[j] &#xff08;两个字符串连接&#xff09;结果等于 target 的下标 (i, j) &#xff08;需满足 i ! j&#xff09;的数目。 示例 1&#xff1a…

What day is that day?(快速幂,打表找周期,或者求通项公式)

有些题怎么都解不出来&#xff0c;这时候可以打表&#xff0c;找规律&#xff0c;求通项公式等&#xff0c;这些方法让人拍手叫绝&#xff0c;真不错…… Description Its Saturday today, what day is it after 11 22 33 ... NN days? Input There are multiple test cas…

mysql中数据定义语言_SQL数据定义语言(DDL)

数据库模式定义语言DDL(DataDefinition Language)&#xff0c;是用于描述数据库中要存储的现实世界实体的语言。一个数据库模式包含该数据库中所有实体的描述定义。这些定义包括结构定义、操作方法定义等。DDL描述的模式&#xff0c;必须由计算机软件进行编译&#xff0c;转换为…

db2 脚本运行错误返回错误原因_电脑运行错误代码大全,遇到报错请自己对照断电原因所在吧...

电脑在运行的时候&#xff0c;会出现一些代码&#xff0c;软件安装不上啊或电脑使用中蓝屏啊出现的代码&#xff0c;每一次出现问题电脑都会以代码的形式反馈&#xff0c;我们就可以凭借错误代码来判断故障源。小编整理了一些常出现的错误代码和问题分享给大家&#xff0c;希望…

LeetCode 2028. 找出缺失的观测数据

文章目录1. 题目2. 解题1. 题目 现有一份 n m 次投掷单个 六面 骰子的观测数据&#xff0c;骰子的每个面从 1 到 6 编号。 观测数据中缺失了 n 份&#xff0c;你手上只拿到剩余 m 次投掷的数据。 幸好你有之前计算过的这 n m 次投掷数据的 平均值 。 给你一个长度为 m 的整…

Sqlite学习笔记(五)SQLite封锁机制

概述 SQLite虽然是一个轻量的嵌入式数据库&#xff0c;但这并不影响它支持事务。所谓支持事务&#xff0c;即需要在并发环境下&#xff0c;保持事务的ACID特性。事务的原子性&#xff0c;隔离性都需要通过并发控制来保证。那么Sqlite的并发控制是怎样的&#xff0c;如何实现&am…

idea中git如何切换到master_IDEA中Git的使用

原文转载于&#xff1a;https://www.cnblogs.com/javabg/p/8567790.html工作中多人使用版本控制软件协作开发&#xff0c;常见的应用场景归纳如下&#xff1a;假设小组中有两个人&#xff0c;组长小张&#xff0c;组员小袁场景一&#xff1a;小张创建项目并提交到远程Git仓库场…

LeetCode 2032. 至少在两个数组中出现的值(哈希/位运算)

文章目录1. 题目2. 解题2.1 哈希查找2.2 位运算1. 题目 给你三个整数数组 nums1、nums2 和 nums3 &#xff0c;请你构造并返回一个 不同 数组&#xff0c;且由 至少 在 两个 数组中出现的所有值组成。 数组中的元素可以按 任意 顺序排列。 示例 1&#xff1a; 输入&#xff1…

LeetCode 2033. 获取单值网格的最小操作数(贪心)

文章目录1. 题目2. 解题1. 题目 给你一个大小为 m x n 的二维整数网格 grid 和一个整数 x 。 每一次操作&#xff0c;你可以对 grid 中的任一元素 加 x 或 减 x 。 单值网格 是全部元素都相等的网格。 返回使网格化为单值网格所需的 最小 操作数。如果不能&#xff0c;返回 …

mysql最左_Mysql最左原则

1. 前言偶然看到一个技术群&#xff0c;对一道关于联合索引的讨论。面试题如下&#xff1a;a_b_c_index 三列复合索引 a 1 and b<100 and c5 这个查询 会用到索引的那几部分&#xff1f;复制代码先说下个人经过本人查询多方资料得到的结论&#xff0c; 只会用到 a 和 b部分(…

LeetCode 2034. 股票价格波动(set + map)

文章目录1. 题目2. 解题1. 题目 给你一支股票价格的数据流。数据流中每一条记录包含一个 时间戳 和该时间点股票对应的 价格 。 不巧的是&#xff0c;由于股票市场内在的波动性&#xff0c;股票价格记录可能不是按时间顺序到来的。 某些情况下&#xff0c;有的记录可能是错的…