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条原则。

  1. 程序次序规则 :一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

  2. 管程锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;

  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

  4. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;

  5. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

  7. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

  8. 传递性:如果操作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内存模型】的更多相关文章

  1. 【多线程与高并发原理篇:4_深入理解synchronized】

    1. 前言 越是简单的东西,在深入了解后发现越复杂.想起了曾在初中阶段,语文老师给我们解说<论语>的道理,顺便给我们提了一句,说老子的无为思想比较消极,学生时代不要太关注.现在有了一定的生 ...

  2. 【多线程与高并发原理篇:1_cpu多级缓存模型】

    1. 背景 现代计算机技术中,cpu的计算速度远远高于主内存的读写速度.为了解决速度不匹配问题,充分利用cpu的性能,在cpu与主内存之间加入了多级缓存,也叫高速缓存,cpu读取数据直接从高速缓存中读 ...

  3. 多线程与高并发(一)—— 自顶向下理解Synchronized实现原理

    一. 什么是锁? 在多线程中,多个线程同时对某一个资源进行访问,容易出现数据不一致问题,为保证并发安全,通常会采取线程互斥的手段对线程进行访问限制,这个互斥的手段就可以称为锁.锁的本质是状态+指针,当 ...

  4. 互联网大厂高频重点面试题 (第2季)JUC多线程及高并发

    本期内容包括 JUC多线程并发.JVM和GC等目前大厂笔试中会考.面试中会问.工作中会用的高频难点知识.斩offer.拿高薪.跳槽神器,对标阿里P6的<尚硅谷_互联网大厂高频重点面试题(第2季) ...

  5. MySQL InnoDB 实现高并发原理

    MySQL 原理篇 MySQL 索引机制 MySQL 体系结构及存储引擎 MySQL 语句执行过程详解 MySQL 执行计划详解 MySQL InnoDB 缓冲池 MySQL InnoDB 事务 My ...

  6. 《深入了解java虚拟机》高效并发读书笔记——Java内存模型,线程,线程安全 与锁优化

    <深入了解java虚拟机>高效并发读书笔记--Java内存模型,线程,线程安全 与锁优化 本文主要参考<深入了解java虚拟机>高效并发章节 关于锁升级,偏向锁,轻量级锁参考& ...

  7. 一篇博客带你轻松应对java面试中的多线程与高并发

    1. Java线程的创建方式 (1)继承thread类 thread类本质是实现了runnable接口的一个实例,代表线程的一个实例.启动线程的方式start方法.start是一个本地方法,执行后,执 ...

  8. 多线程与高并发(三)synchronized关键字

    上一篇中学习了线程安全相关的知识,知道了线程安全问题主要来自JMM的设计,集中在主内存和线程的工作内存而导致的内存可见性问题,及重排序导致的问题.上一篇也提到共享数据会出现可见性和竞争现象,如果多线程 ...

  9. Java 面试知识点解析(二)——高并发编程篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

随机推荐

  1. 🍛 餐厅吃饭版理解 IO 模型:阻塞 / 非阻塞 / IO 复用 / 信号驱动 / 异步

    IO 概念 一个基本的 IO,它会涉及到两个系统对象,一个是调用这个 IO 的进程对象,另一个就是系统内核 (kernel).当一个 read 操作发生时,它会经历两个阶段: 通过 read 系统调用 ...

  2. IDEA使用Docker插件构建镜像

    IDEA使用Docker插件构建镜像 记一次坑  第一次插件docker-maven-plugin的 配置文件中没写远程主机的地址 <dockerHost>http://192.168.1 ...

  3. Linux 系统下你关注过哪些内核参数,说说你知道的?

    Tcp/ip io cpu memorynet.ipv4.tcp_syncookies = 1#启用syncookiesnet.ipv4.tcp_max_syn_backlog = 8192#SYN队 ...

  4. Spring Boot 需要独立的容器运行吗?

    可以不需要,内置了 Tomcat/ Jetty 等容器.

  5. springmvc对前台参数的接收

    一.基本数据类型的接收 代码: @RequestMapping("/selectRegion") public BaseResult<?> method(String ...

  6. jQuery对象的创建

    1.js类库 JavaScript封装了很多的预定义的对象和实用函数,能帮助使用者建立有高难度交互 客户端页面,并且兼容各大浏览器.跑在浏览器,请求服务器 当前比较流行的js库: jquery EXT ...

  7. Python - 分支循环、可迭代对象与迭代器

  8. 全方位讲解 Nebula Graph 索引原理和使用

    本文首发于 Nebula Graph Community 公众号 index not found?找不到索引?为什么我要创建 Nebula Graph 索引?什么时候要用到 Nebula Graph ...

  9. 汽车最强大脑ECU和单片机是什么关系

    先上图一张,据说这是某个F1赛车的动力总成ECU. 定睛一看,这不就是两个英飞凌的单片机的合体嘛. ECU的定义 ECU原来指的是engine control unit,即发动机控制单元,特指电喷发动 ...

  10. Numpy非常重要有用的数组合并操作

    Numpy非常重要有用的数组合并操作 背景:在给机器学习准备数据的过程中,经常需要进行不同来源的数据合并的操作. 两类场景: 给已有的数据添加多行,比如增添一些样本数据进去: 给已有的数据添加多列,比 ...