一、Java内存模型

硬件处理

电脑硬件,我们知道有用于计算的cpu、辅助运算的内存、以及硬盘还有进行数据传输的数据总线。在程序执行中很多都是内存计算,cpu为了更快的进行计算会有高速缓存,最后同步至主内存,大概的交互如下图

 
 

为了使处理器内部的运算单元能够被充分的利用,处理器可能会对输入代码进行乱序执行优化,然后将计算后的结果进行重组,保证该结果和顺序执行的结果是一致的(单位时间内,一个core只能执行一个线程,所以结果的一致仅限一个线程内)。

Java内存模型

Java内存模型是语言级别的模型,它的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取数变量这样的底层细节。

在内存里,java内存模型规定了所有的变量都存储在主内存(物理内存)中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行。不同的线程无法访问别线程的工作内存里的内容。下图展示了逻辑上 线程、主内存、工作内存的三者交互关系。

 
 

内存交互操作

Java内存模型定义的8个操作指令来进行内存之间的交互

read 读取主内存的值,并传输至工作内存
load 将read的变量值存放到工作内存

read 好比快递运输车,工作内存好比站点,运输车将快递运输到站点,站点必须得卸货 load

use 将工作内存的变量值,传递给执行引擎
assign 执行引擎对变量进行赋值

use好比站点进行快递员分配,站点说我把快递分给你了快递员A。快递员A接收到快递assign开始派送。

store 工作内存将变量传输到主内存
write 主内存将工作内存传递过来的变量进行存储

storewrite就好理解了,快递员A将快递送到你家门口(store),然后你得签收(write)

lock 用作主内存变量,它把一个变量在内存里标识为一个线程独占状态
unlock 用作主内存变量,它对被锁定的变量进行解锁

下图展示下工作内存和主内存间的指令操作交互

 
 

指令规则

  • read 和 load、store和write必须成对出现
  • assign操作,工作内存变量改变后必须刷回主内存
  • 同一时间只能运行一个线程对变量进行lock,当前线程lock可重入,unlock次数必须等于lock的次数,该变量才能解锁
  • 对一个变量lock后,会清空该线程工作内存变量的值,重新执行load或者assign操作初始化工作内存中变量的值。
  • unlock前,必须将变量同步到主内存(store/write操作)

二、重排序

从java源码到最终实际执行的指令序列,会经历下面3种重排序

 
从源码到最终执行的指令序列的示意图

重排序的现象
a=1,b=a 这一组 b依赖a,不会重排序;
a=1,b=2 这一组 a和b没有关系,那么就有可能被重排序执行 b=2,a=1

  • 编译器优化的重排序
    编译器在不改变单线程程序语义的前提下,可以重新安排语句执行顺序

  • 指令级并行的重排序
    现代处理器采用了指令级并行技术将多条指令重叠执行。在不存在数据依赖的时候,处理器可以改变指令执行顺序

  • 内存系统重排序
    处理器使用高速缓存,使得多运算单元加载和存储主内存操作看上去可能在乱序执行

重排序的代码示例,文章底部的参考文章里有示例,这里就不罗列了。

三、内存屏障

JMM(java 内存模型) 在不改变程序执行结果的前提下,尽可能的支持处理器的重排序。通过禁止特定特定类型的编译器重排序和处理器重排序,为开发者提供一致的内存可见性保证,如 volatilefinal

Java编译器在生成指令的时候会在适当位置插入内存屏障来进制特定类型的处理器排序。

内存屏障说的通俗一点就是一个栏杆,在两个指令之间插入栏杆,后面的指令就不能越过栏杆先执行。

JMM定义的内存屏障指令分为4类

  • LoadLoad
    指令示例 Load1LoadLoadLoad2
    确保Load1数据装载一定先于Load2及后续所有Load指令

  • LoadStore
    指令示例 Load1LoadStoreStore2
    确保Load1数据装载一定先于Store2及后续所有Store指令

  • StoreStore
    指令示例 Store1StoreStoreStore2
    确保Store1主内存落地(从工作内存刷入主存,其它线程可见)一定先于Store2及后续所有Store指令

  • StoreLoad
    指令示例 Store1StoreLoadLoad2
    确保Store1主内存落地(从工作内存刷入主存,其它线程可见)一定先于Load2及后续所有Load指令

处理器对重排序的支持

 
 

从上面可以看到不同的处理器架构对重排序的支持也是不一样(其它处理器架构暂不罗列),所以不同的平台JMM的内存屏障施加也略有不同,具体来说,比如 X86 对Load1Load2不支持重排序,那么你就没有必要施加 LoadLoad屏障。

四、volatile的内存语义

volatile我们都知道是java的关键字用来保证数据可见性,防止指令重排的效果。包括JUC里AQS Lock的底层实现也是基于volatitle来实现。

volatile写的内存语义

当写一个volatile变量的时候,JMM会把该线程对应的本地内存变量值刷新到主内存

volatile读的内存语义

当读一个volatile变量的时候,JMM会把线程本次内存置为无效。线程接下来将从主内存中读取共享变量(也就是重新从主内存获取值,更新运行内存中的本地变量)

上面两个语义,保证了volatile变量写入对线程的可见性

volatile内存屏障插入规则

 
volatile内存屏障策略

代码简单示例

 class X {
int a, b;
volatile int v, u; void f() {
int i, j; i = a;// load a 普通load
j = b;// load b 普通load
i = v;// load v volatile load
// LoadLoad
j = u;// load u volatile load
// LoadStore
a = i;// store a 普通store
b = j;// store b 普通store
// StoreStore
v = i;// store v volatile store
// StoreStore
u = j;// store u volatile store
// StoreLoad
i = u;// load u volatile load
// 两个屏障 LoadLoad 和 LoadStore
j = b;// load b 普通load
a = i;// store a 普通store
}
}

上述代码可以套用volatile屏障规则对应。

当然不同的处理器架构重排序的支持也是不一样,比如X86 只有当 store1 load2 的时候会进行重排序,那么就会省略掉很多类型的内存屏障。

五、final的内存语义

final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。
被final修饰的变量不能被修改,方法不能被重写,类不能被继承。

我们暂时把final修饰的称作域,对于final域,编译器和处理器要遵守两个重排序规则

写规则

  • 在构造函数内对一个final域的写入,与随后把这个被构造的对象的引用赋值给一个引用变量,这两个操作不可重排序

JMM禁止编译器把final域的写重排序到构造函数之外
编译器会在final域写入的后面插入StoreStore屏障,禁止处理器把final域的写重排序到构造函数之外。

该规则可以保证在对象引用为任意线程可见之前,对象的final域已经被正确初始化,而普通域无法保障。

读规则

  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。

该规则保证在读一个对象的final域之前,一定会先读包含这个域的对象引用。


参考资料

聊聊Java内存模型的更多相关文章

  1. Java并发(1)- 聊聊Java内存模型

    引言 在计算机系统的发展过程中,由于CPU的运算速度和计算机存储速度之间巨大的差距.为了解决CPU的运算速度和计算机存储速度之间巨大的差距,设计人员在CPU和计算机存储之间加入了高速缓存来做为他们之间 ...

  2. 面试官:为什么需要Java内存模型?

    面试官:今天想跟你聊聊Java内存模型,这块你了解过吗? 候选者:嗯,我简单说下我的理解吧.那我就从为什么要有Java内存模型开始讲起吧 面试官:开始你的表演吧. 候选者:那我先说下背景吧 候选者:1 ...

  3. 聊聊高并发(三十四)Java内存模型那些事(二)理解CPU快速缓存的工作原理

    在上一篇聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了Java内存模型是一个语言级别的内存模型抽象.它屏蔽了底层硬件实现内存一致性需求的差异,提供了对上层的 ...

  4. 3.java内存模型以及happens-before规则

    1. JMM的介绍 在上一篇文章中总结了线程的状态转换和一些基本操作,对多线程已经有一点基本的认识了,如果多线程编程只有这么简单,那我们就不必费劲周折的去学习它了.在多线程中稍微不注意就会出现线程安全 ...

  5. Java内存模型(Java Memory Model,JMM)

    今天简单聊聊什么叫做 Java 内存模型,不是 JVM 内存结构哦. JMM 是一个语言级别的内存模型,处理器的硬件模型是硬件级别,Java中的内存模型是内存可见性的基本保证.从而为我们 volati ...

  6. Java内存模型以及happens-before规则

    本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...

  7. Java内存模型之总结

    经过四篇博客阐述,我相信各位对Java内存模型有了最基本认识了,下面LZ就做一个比较简单的总结. 总结 JMM规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序.一方面,要为 ...

  8. 一夜搞懂 | Java 内存模型与线程

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习内存模型与线程? 并发处理的广泛应用是 Amdah1 定律代替摩尔定律成为计 ...

  9. 深入浅出Java内存模型

    面试官:我记得上一次已经问过了为什么要有Java内存模型 面试官:我记得你的最终答案是:Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台 ...

随机推荐

  1. react项目开发中遇到的问题

    前言 作为一个前端爱好者来说,都想在react上一试生手,那么在搭建react项目开发时,肯定会有这样或者那样的问题,尤其是对初学者来说,下面就个人在开发过程中遇到的问题总结一下,好在有google帮 ...

  2. Linux系统日志分析与管理(14)

    当你的 Linux 系统出现不明原因的问题时,你需要查阅一下系统日志才能够知道系统出了什么问题了,所以说了解系统日志是很重要的事情,系统日志可以记录系统在什么时间.哪个主机.哪个服务.出现了什么信息等 ...

  3. 一次Java解析数独的经历

    1. 背景 中午下楼去吃饭,电梯里看到有人在玩数独,之前也玩过,不过没有用程序去解过,萌生了一个想法,这两天就一直想怎么用程序去解一个数独.要去解开一个数独,首先要先了解数独的游戏规则,这样才能找到对 ...

  4. POJ 2498

    #include<iostream> using namespace std; #include<string> #include<stdio.h> int mai ...

  5. 【原创】SQL Server 性能调优读书笔记

    CPU 100%: 有时可能是硬盘性能不足,或者内存容量不够,让CPU一直忙于I/O. 导致性能问题的一些因素: 用户习惯:在运行尖峰时刻做一些不必做但消耗资源的事情,如之行数据库完整备份,如在服务器 ...

  6. Orleans实战目录

    一 项目结构 1> 接口项目 .net core类库 2> Grains实现项目 .net core类库 3> 服务Host .net core console applicatio ...

  7. Math、Random、System、BigInteger、Date、DateFormat、Calendar类,正则表达式_DAY14

    1:Math&大数据类四则运算 X abs(X x) double random()         产生随机数 double ceil(double a)   向上取整 double flo ...

  8. vtk文件编写

    在paraview中加载vtk文件,可以很好的显示三维空间图像,如下cpp代码: #include <iostream> #include <fstream> #include ...

  9. node爬虫gbk中文乱码问题

    刚入坑node 写第二个node爬虫时,遇到了这个坑,记录一下. 主要步骤: 1.安装iconv-lite 输入npm install iconv-lite 2.将接收到的网页源码以二进制的方式存储下 ...

  10. 解决docker镜像无法下载的问题

    从daocloud.io中找到了获取镜像的方式,在镜像仓库中可以找到镜像的地址,其他镜像地址可以以此类推: # docker pull daocloud.io/library/centos:lates ...