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. 一文带你了解Lakehouse的并发控制:我们是否过于乐观

    1. 概述 如今数据湖上的事务被认为是 Lakehouse 的一个关键特征. 但到目前为止,实际完成了什么? 目前有哪些方法? 它们在现实世界中的表现如何? 这些问题是本博客的重点. 有幸从事过各种数 ...

  2. zookeeper有几种部署模式? zookeeper 怎么保证主从节点的状态同步?

    一.zookeeper的三种部署模式 Zookeeper 有三种部署模式分别是单机模式.伪集群模式.集群模式.这三种模式在不同的场景下使用: 单机部署:一般用来检验 Zookeeper 基础功能,熟悉 ...

  3. 什么是 Spring Batch?

    Spring Boot Batch 提供可重用的函数,这些函数在处理大量记录时非常重要,包括日志/跟踪,事务管理,作业处理统计信息,作业重新启动,跳过和资源管理.它还提供了更先进的技术服务和功能,通过 ...

  4. Gradle 使用@Value注册编译报错

    报错信息:Expected '$(student - name)' to be an inline constant of type java.lang.String in @org.springfr ...

  5. 手撕代码:leetcode 309最佳买卖股票时机含冷冻期

    转载于:https://segmentfault.com/a/1190000014746613 给定一个整数数组,其中第i个元素代表了第i天的股票价格. 设计一个算法计算出最大利润.在满足以下约束条件 ...

  6. Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?

    可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来 实现事件监听和回调.

  7. Kafka 的设计架构你知道吗?

    Producer :消息生产者,就是向 kafka broker 发消息的客户端. Consumer :消息消费者,向 kafka broker 取消息的客户端. Topic :可以理解为一个队列,一 ...

  8. HTML、JavaScript、Java、CSS它们的注释有哪些相同和不同?

    <!--html--> /*css*/ //javascript /*javascript*/

  9. Java 中 WeakReference 与 SoftReference 的区别?

    虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率, 但是 WeakReference ,一旦失去最后一个强引用,就会被 GC 回收,而软引用 虽然不 ...

  10. scanf()格式化输入

    scanf();有种带[]的格式化输出方式 此格式控制符的基本格式为:%[scanfset] #include<stdio.h> int main() { char str[100] ; ...