【并发编程】一文带你读懂深入理解Java内存模型(面试必备)
并发编程这一块内容,是高级资深工程师必备知识点,25K起如果不懂并发编程,那基本到顶。但是并发编程内容庞杂,如何系统学习?本专题将会系统讲解并发编程的所有知识点,包括但不限于:
线程通信机制,深入JMM内存模型原理,深入synchronized原理,深入volatile原理,DCL,详解AQS,CAS,可重入锁,读写锁原理,详解并发工具类,深入理解threadLocal,Fork、Join,原子类详解,Java并发集合详解(ConcurrentHashMap,ConcurrentLinedQueue,ConcurrentListMap等),阻塞队列深入探究,深入线程池原理及其设计思想。
本文为深入理解java内存模型。
零、全文思维导图
主线如上图红色箭头,大家可以先看看整体讲的是什么。java内存模型前面是铺垫,后面是相关内容。

一、引出java内存模型(不做重点讲解)

二、那什么才会用到java内存模型?
共享变量(实例域,静态域,数组元素)才会用到。
局部变量,方法定义参数等不会在线程间共享,所以他们不会有内存可见性问题,也不受内存模型影响
三、java内存模型抽象示意图
Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。

3.1 主内存(Main Memory)
主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。
3.2 本地内存(Working Memory)
本地内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。为啥有本地内存这个概念?因为直接操作主内存太慢了
通过一系列内存读写的操作指令(JVM内存模型共定义了8种内存操作指令,以后会细讲),线程A把静态变量 s=0 从主内存读到工作内存,再把 s=3 的更新结果同步到主内存当中。从单线程的角度来看,这个过程没有任何问题。
四、指令重排序
理解重排序前这个概念前,我们先转换场景,从java内存模型走出来,来到硬件CPU这个维度。
4.1基本概念:
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序(简单理解就是原本我们写的代码指令执行顺序应该是A→B→C,但是现在的CPU都是多核CPU,为了秀下优越,为了提高并行度,为了提高性能等,可能会出现指令顺序变为B→A→C等其他情况)。
当然CPU们也不是随便就去重排序,需要满足以下两个条件(遵循的规则):
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序
4.2重排序分三类:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

那么重排序会遵循什么样的规则?
五、as-if-serial
5.1as-if-serial语义的意思是:
不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。OK,这就相当于给CPU们定下规则。不要随便重排序。要满足我这个as-if-serial的前置条件,才能重排序。
5.2
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使程序员不必担心单线程中重排序的问题干扰他们,也无需担心内存可见性问题。
注意:as-if-serial只保证单线程环境,多线程环境下无效。那多线程,并发编程下怎么办?
六、多线程下导致的问题及解决办法
上面的这些重排序都可能导致多线程程序出现内存可见性问题,JMM那么如何解决?
- 对于编译器重排序,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
- 对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
七、什么是内存屏障?
7.1 内存屏障(Memory Barrier)是一种CPU指令‘。
内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。
7.2 实际运用场景:
volatile便是基于内存屏障实现的。
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。这个指令就相当于一个内存屏障。具体表现为:
- 当写一个volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
- 当读一个volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
从而保证了,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。
(注,关于volatile会在后面单章讲解,这里不过于赘婿)
八、happens-before原则
从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。
换句话说,在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
happens-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。
这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。
摘录一些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关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。
那么说了那么多规则,来看看happens-before与JMM的关系

九、as-if-serial和happens-before小结
- as-if-serial语义保证单线程内程序的执行结果不被改变
- happens-before关系保证正确同步的多线程程序的执行结果不被改变。
- 其实都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
十、扯了那么久,这几者如何理解?结论:
- 重排序是多核CPU等为了性能进行的优化操作,但会导致可见性等问题。为了解决这些问题,所以JMM需要制定一些规则,不让其随意重排序。
- as-if-serial只保证单线程环境的不可随意重排序,那么多线程下呢?
- 所以有了happens-before原则,其是JMM(JSR-133内存模型)的规范之一。
- 内存屏障是CPU指令。
- 所以说,happens-before是JMM制定的最终目的,内存屏障则是实现happens-before的具体手段。

END
彩蛋福利
部分资料如下:

【并发编程】一文带你读懂深入理解Java内存模型(面试必备)的更多相关文章
- 一文带你读懂什么是vxlan网络
一个执着于技术的公众号 一.背景 随着云计算.虚拟化相关技术的发展,传统网络无法满足大规模.灵活性要求高的云数据中心的要求,于是便有了overlay网络的概念.overlay网络中被广泛应用的就是vx ...
- 一文带你读懂zookeeper在大数据生态的应用
一个执着于技术的公众号 一.简述 在一群动物掌管的世界中,动物没有人类聪明的思想,为了保持动物世界的生态平衡,这时,动物管理员-zookeeper诞生了. 打开Apache zookeeper的官网, ...
- 实战 | 一文带你读懂Nginx反向代理
一个执着于技术的公众号 前言 在前面的章节中,我们已经学习了nginx基础知识: 给小白的 Nginx 10分钟入门指南 Nginx编译安装及常用命令 完全卸载nginx的详细步骤 Nginx 配置文 ...
- Java并发指南2:深入理解Java内存模型JMM
本文转载自互联网,侵删 一:JMM基础与happens-before 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实 ...
- 用实例带你深入理解Java内存模型
摘要:本文的目的来理解 J V M 与我们的内存两者之间是如何协调工作的. 本文分享自华为云社区<一文带你图解Java内存模型>,作者: 龙哥手记 . 我们今天要特别重点讲的,也就是我们本 ...
- 从源码入手,一文带你读懂Spring AOP面向切面编程
之前<零基础带你看Spring源码--IOC控制反转>详细讲了Spring容器的初始化和加载的原理,后面<你真的完全了解Java动态代理吗?看这篇就够了>介绍了下JDK的动态代 ...
- 一文带你读懂 Mysql 和 InnoDB存储引擎
作为一名开发人员,在日常的工作中会难以避免地接触到数据库,无论是基于文件的 sqlite 还是工程上使用非常广泛的 MySQL.PostgreSQL,但是一直以来也没有对数据库有一个非常清晰并且成体系 ...
- 一文带你读懂!华为云在ACMUG技术沙龙上都透露了些啥?
摘要:近日,华为云数据库业务总裁苏光牛在ACMUG中国MySQL用户组主办的 "华为云专场" 技术沙龙中分享了华为云数据库重磅新品GaussDB的核心能力与竞争优势.那么, Gau ...
- 一文带你读懂什么是docker
一 简介 1.了解Docker的前生LXC LXC为Linux Container的简写.一种轻量级的内核虚拟化技术,隔离进程和资源. Linux Container有点像chroot,提供了一个拥有 ...
随机推荐
- 《C prime plus (第五版)》 ---第11章 字符串和字符串函数---4
字符串的例子: 1.字符串排序: 应用范围:准备花名册,建立索引以及很多情况下都会用刀字符串的排序.这个程序的主要工具就是strcmp(). 算法:读一个字符串数组,对它们进行排序并输出. #incl ...
- nginx 部署thinkphp 做的东西, 配置 Pathinfo 模式,终于配好了
用thinkphp做了一个网站,部署到ubuntu上的时候需要,服务器用的是nginx,本身不支持pathinfo模式,需要修改配置文件,使其能够用pathinfo. 我用的是虚拟主机的方式.之前配置 ...
- linux 下监控进程流量情况命令 NetHogs
摘自: http://www.cnblogs.com/kerrycode/p/4748970.html NetHogs介绍 NetHogs是一款开源.免费的,终端下的网络流量监控工具,它可监控Linu ...
- Python成长之路第一篇(2)__初识列表和元组
可以将列表和元组当成普通的“数组”,他能保存任意数量任意类型的Python对象,和数组一样都是通过数字0索引访问元素,列表和元组可以存储不同类型的对象,列表和元组有几处重要区别.列表元素用([])包括 ...
- 深入理解JVM - 虚拟机字节码执行引 - 第八章
概述从外观上看起来,所有的 Java 虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果.主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行. 运行时 ...
- 利用Python+阿里云实现DDNS(动态域名解析)
引子我想大家应该都很熟悉DNS了,这回在DNS前面加了一个D又变成了什么呢?这个D就是Dynamic(动态),也就是说,按照传统,一个域名所对应的IP地址应该是定死的,而使用了DDNS后,域名所对应的 ...
- 菜鸟快速自学java00之变量类型
---恢复内容开始--- 菜鸟快速自学java00之变量类型 一.诉苦 自己成为了Java中的一只菜鸟,而且已经菜了好多天了,我为什么会这么菜?归根结底,还是觉得自己在累计知识的同时,没有做好笔记,导 ...
- scala初步
1 安装 http://scala-ide.org/ http://scala-lang.org/
- [acm]HDOJ 2064 汉诺塔III
题目地址: http://acm.hdu.edu.cn/showproblem.php?pid=2064 汉诺塔变种,只能从中间专业,递归关系为:f(n)=3*f(n-1)+2. //汉诺塔变种,只能 ...
- java-04 数组和二维数组
java 中内存分配地址值以及栈和堆得区别: ##########数组操作的两个常见小问题(越界和空指针)############## 数组索引越界异常,访问了不存在的索引: 空指针: ####### ...