【多线程与高并发原理篇:3_java内存模型】
1. 概述
Java 内存模型即 Java Memory Model,简称 JMM。从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系,线程之间的共享变量存储在主内存中,每个线程都有一个私有的工作内存,工作内存中存储了该线程以读/写共享变量的副本。工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java内存模型是跟cpu缓存模型是类似的,基于cpu缓存模型来建立的Java内存模型,只不过Java内存模型是标准化的,屏蔽掉底层不同的计算机的区别。
2. Java内存模型带来的问题
Java内存模型规定了线程对主内存的操作具备原子性,包括以下8个操作:
lock:主内存,标识变量为线程独占;
unlock:主内存,解锁线程独占变量;
read:主内存,读取内存到线程缓存(工作内存);
load:工作内存,read后的值放入线程本地变量副本;
use:工作内存,传值给执行引擎;
assign:工作内存,执行引擎结果赋值给线程本地变量;
store:工作内存,存值到主内存给write备用;
write:主内存,写变量值。
假设如下程序,两个未加同步控制的线程去同时对i自增,会出现什么结果呢?
public class Test {
private int i = 0;
public void increment() {
i++;
System.out.println("i=" + i);
}
public static void main(String[] args) {
Test t = new Test();
new Thread(() -> t.increment()).start();
new Thread(() -> t.increment()).start();
}
}
通过运行会出现下面三种情况
i=1
i=1
或者
i=1
i=2
或者
i=2
i=2
下面通过图来解释第一种情况

A、B两个线程都有自己的工作内存,A自从执行read操作,从主内存读取i=0,随后load操作载入自己的工作内存,接着执行use操作,对i进行自增,然后从新赋值操作assign,此时线程A的工作内存i=1,随后store操作进行存储,最后写回到主内存,最终i=1。
B线程也进行如此操作,read->load->use->assign->store->write,最终也得出i=1。
出现第二种,关键在于B线程read操作是从A线程刷新到主内存后才去取值的。执行顺序是:线程A自增->线程A打印i最终值->线程B自增->线程B打印i最终值,如下图

出现第三种,是线程A自增后把i=1刷新到主内存,在执行打印之前,线程B优先从主内存获取i=1,进行read->load->use->assign->store->write,将i=1自增为i=2,随后线程A执行打印操作,执行顺序是:线程A自增->线程B自增->线程A打印i最终值->线程B打印i最终值,如下图

3. 可见性、有序性、原子性
虽然java内存模型JMM提供为每个线程提供了每个工作内存,存放共享变量的变量副本,但是如果线程没有作可见性的控制,从上述过程中可以看出,多线程下对共享变量的修改,其结果依然是不可预知的。
3.1 可见性
volatile关键词,在程序级别,保证对一个共享变量的修改对另外线程立马可见。上述程序对i加入volatile关键字,可以保证能始终得到第二种结果。
下面用程序来演示:
Class VolatileExample {
int a = 0;
volatile boolean flg = false;
public void writer() {
a = 1;
flg = true;
}
public void reader() {
if (flg) {
int i = a;
......
}
}
}
图解如下:

上述过程概括为两句话:
当写一个volatile修饰的变量时,JMM会把线程对应的本地内存中的共享变量值刷新的主内存;
当读一个volatile修饰的变量时,JMM会把该线程对应的本地内存置为无效,从主内存读取最新的共享变量的值。
上述过程解释了volatile的可见性问题。
3.2 有序性
对于一些代码,编译器或者处理器,为了提高代码执行效率,会将指令重排序,就是说比如下面的代码:
flg = false;
//线程1:
parpare(); // 准备资源
flg = true;
//线程2:
while(!flg) {
Thread.sleep(1000);
}
execute();// 基于准备好的资源执行操作
重排序之后,让flag = true先执行了,会导致线程2直接跳过while等待,执行某段代码,结果prepare()方法还没执行,资源还没准备好呢,此时就会导致代码逻辑出现异常。
volatile通过内存屏障,保证volatile修饰的变量,与其前后定义的值,不发生指令重排。JMM定义了如下四种内存屏障StoreStore、StoreLoad、LoadLoad、LoadStore;
对于volatile写,在前面插入StoreStore,禁止上面的普通读与下面的volatile写重排序;后面插入StoreLoad,禁止上面的volatile写与下面的普通读重排序,如下图:

对于volatile读,在后面插入LoadLoad,禁止上面的volatile读与下面的普通读重排序;下面再插入LoadStore,禁止上面的volatile读与下面的普通写重排序,如下图:

happens-before原则
为了保证多线程之间在某些情况下一定不能发生指令重排,java内存模型规定了8条原则。
程序次序规则 :一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
管程锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
3.3 原子性
一般情况下,volatile修饰的变量是不能保证原子性的,例如i++是复合操作,先读取,再修改变量的值,是不具备原子性的
4. volatile作用
通过上面的描述,可以得出volatile的作用主要有两点:
- 保证线程可见性
- 禁止指令重排序
5. HotSpot层面实现
通过hsdis工具查看java汇编文件,首先下载hsdis-amd64.dll到 \jdk1.8\jre\bin ,然后设置VM参数,-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
最终执行时会在volatile变量前加如下信息
lock addl $0x0,(%rsp)
如下图:

6. 底层CPU硬件层面实现
上述过程中,JVM虚拟机会向CPU发送lock前置指令,将这个变量所在的缓存行数据写回主内存,如果其他CPU缓存的值是旧值,就会有问题,在多CPU(这里指多个核)下,每个CPU都会通过嗅探总线上传播的数据是否与自己的缓存一致,通过缓存一致性协议,最终保证多个CPU内部缓存数据的一致性,下面通过图来说明。

虚拟机的lock前缀指令,在底层硬件是通过缓存一致性协议来完成的,不同的CPU缓存一致性协议不一样, 有MSI、MESI、MOSI、Synapse、Firefly及Dragon,英特尔CPU的缓存一致性协议是通过MESI来完成的。
为了实现MESI协议,需要解释两个专业术语:flush处理器缓存、refresh处理器缓存。
flush处理器缓存,他的意思就是把自己更新的值刷新到高速缓存里去(或者是主内存),因为必须要刷到高速缓存(或者是主内存)里,才有可能在后续通过一些特殊的机制让其他的处理器从自己的高速缓存(或者是主内存)里读取到更新的值。除了flush以外,他还会发送一个消息到总线(bus),通知其他处理器,某个变量的值被他给修改了。
refresh处理器缓存,他的意思就是说,处理器中的线程在读取一个变量的值的时候,如果发现其他处理器的线程更新了变量的值,必须从其他处理器的高速缓存(或者是主内存)里,读取这个最新的值,更新到自己的高速缓存中。所以说,为了保证可见性,在底层是通过MESI协议、flush处理器缓存和refresh处理器缓存,这一整套机制来保障的。
flush和refresh,这两个操作,flush是强制刷新数据到高速缓存(主内存),不要仅仅停留在写缓冲器里面;refresh,是从总线嗅探发现某个变量被修改,必须强制从其他处理器的高速缓存(或者主内存)加载变量的最新值到自己的高速缓存里去。
7. 总结
本篇主要讲述了Java内存模型的作用,屏蔽了底层实现的细节,同时带来了一系列问题,导致线程之间的三大问题,即有序性、可见性、原子性,volatile关键字修饰的变量在多线程之间的作用,以及初步分析了底层是如何实现的,如果要深入分析,这个得具体看MESI协议规范,以及不同硬件底层的实现逻辑,比如英特尔的操作手册,后面有时间再接着深入。
【多线程与高并发原理篇:3_java内存模型】的更多相关文章
- 【多线程与高并发原理篇:4_深入理解synchronized】
1. 前言 越是简单的东西,在深入了解后发现越复杂.想起了曾在初中阶段,语文老师给我们解说<论语>的道理,顺便给我们提了一句,说老子的无为思想比较消极,学生时代不要太关注.现在有了一定的生 ...
- 【多线程与高并发原理篇:1_cpu多级缓存模型】
1. 背景 现代计算机技术中,cpu的计算速度远远高于主内存的读写速度.为了解决速度不匹配问题,充分利用cpu的性能,在cpu与主内存之间加入了多级缓存,也叫高速缓存,cpu读取数据直接从高速缓存中读 ...
- 多线程与高并发(一)—— 自顶向下理解Synchronized实现原理
一. 什么是锁? 在多线程中,多个线程同时对某一个资源进行访问,容易出现数据不一致问题,为保证并发安全,通常会采取线程互斥的手段对线程进行访问限制,这个互斥的手段就可以称为锁.锁的本质是状态+指针,当 ...
- 互联网大厂高频重点面试题 (第2季)JUC多线程及高并发
本期内容包括 JUC多线程并发.JVM和GC等目前大厂笔试中会考.面试中会问.工作中会用的高频难点知识.斩offer.拿高薪.跳槽神器,对标阿里P6的<尚硅谷_互联网大厂高频重点面试题(第2季) ...
- MySQL InnoDB 实现高并发原理
MySQL 原理篇 MySQL 索引机制 MySQL 体系结构及存储引擎 MySQL 语句执行过程详解 MySQL 执行计划详解 MySQL InnoDB 缓冲池 MySQL InnoDB 事务 My ...
- 《深入了解java虚拟机》高效并发读书笔记——Java内存模型,线程,线程安全 与锁优化
<深入了解java虚拟机>高效并发读书笔记--Java内存模型,线程,线程安全 与锁优化 本文主要参考<深入了解java虚拟机>高效并发章节 关于锁升级,偏向锁,轻量级锁参考& ...
- 一篇博客带你轻松应对java面试中的多线程与高并发
1. Java线程的创建方式 (1)继承thread类 thread类本质是实现了runnable接口的一个实例,代表线程的一个实例.启动线程的方式start方法.start是一个本地方法,执行后,执 ...
- 多线程与高并发(三)synchronized关键字
上一篇中学习了线程安全相关的知识,知道了线程安全问题主要来自JMM的设计,集中在主内存和线程的工作内存而导致的内存可见性问题,及重排序导致的问题.上一篇也提到共享数据会出现可见性和竞争现象,如果多线程 ...
- Java 面试知识点解析(二)——高并发编程篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
随机推荐
- 如何理解Node.js和JavaScript的关系
一.Javascript的引擎 浏览器一般有两个引擎,一个是Html引擎,一个是脚本引擎. JavaScript是一种脚本语言,最初用于浏览器的动态显示,方便操作页面数据和内容.但实际上,它也可以在浏 ...
- Vue中图片的加载方式
一.前言 VUE项目中图片的加载是必须的,那么vue中图片的加载方式有哪些呢,今天博主就抽点时间来为大家大概地捋一捋. 二.图片的加载方法 1.在本地加载图片(静态加载) 图片存放assets文件夹中 ...
- 集成SpringCloudBus,但没有总线通知更改
配置服务端别忘了添加以下2个依赖 implementation("org.springframework.cloud:spring-cloud-config-server")imp ...
- 服务注册和发现是什么意思?Spring Cloud 如何实现?
当我们开始一个项目时,我们通常在属性文件中进行所有的配置.随着越来越多的服务开发和部署,添加和修改这些属性变得更加复杂.有些服务可能会下降,而某些位置可能会发生变化.手动更改属性可能会产生问题.Eur ...
- 我们能在 Switch 中使用 String 吗?
从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法 糖.内部实现在 switch 中使用字符串的 hash code. 30.Java 中的构造器链是什么? ...
- Linux编译安装软件常见问题及排查
1.配置cmake参数时提示: The C compiler identification is unknown. The CXX compiler identification is unknown ...
- 使用 Docker, 7 个命令部署一个 Mesos 集群
这个教程将给你展示怎样使用 Docker 容器提供一个单节点的 Mesos 集群(未来的一篇文章将展示怎样很容易的扩展这个到多个节点或者是见底部更新).这意味着你可以使用 7 个命令启动整个集群!不需 ...
- 学习Kvm(四)
安装KVM虚拟化 1.系统基础环境: [root@linux-node1 ~]# ip addr | grep inet | awk '{ print $2; }' | sed 's/\/.*$//' ...
- Spark学习摘记 —— RDD行动操作API归纳
本文参考 参考<Spark快速大数据分析>动物书中的第三章"RDD编程",前一篇文章已经概述了转化操作相关的API,本文再介绍行动操作API 和转化操作API不同的是, ...
- 小程序输入框闪烁BUG解决方案
前言 本人所说的小程序,都是基于mpvue框架而上的,因此BUG可能是原生小程序的,也有可能是mpvue的. 问题描述 在小程序input组件中,如果使用v-model进行双向绑定,在输入时会出现光标 ...