1.JAVA 的并发模型

共享内存模型

在共享内存的并发模型里面,线程之间共享程序的公共状态,线程之间通过读写内存中公共状态来进行隐式通信

该内存指的是主内存,实际上是物理内存的一小部分

2.JAVA 内存模型的抽象

2.1 java内存中哪些数据是线程安全的,哪些是非安全的

  1. 非线程安全 : 在 java 中所有的实例域、静态域、和数组元素都存放在堆内存中,并且这些数据是线程共享的,所以会存在内存可见性问题
  2. 线程安全 : 局部变量、方法定义的参数、异常处理器参数是当前线程虚拟机栈中的数据,并且不会进行线程共享,所以不会存在内存可见性问题

2.2 线程间通讯的本质

  1. 线程间通讯的本质是 :JMM即 JAVA 内存模型进行控制,JMM决定了一个线程对共享变量的写入何时对其他线程可见。

由上图能看出来线程间的通讯都是通过主内存来进行传递消息的, 每个线程在进行共享数据处理的时候都是将共享的数据复制到当前线程本地(每个线程自己都有一个内存)来进行操作。

  1. 消息通讯过程(不考虑数据安全性的问题) :
  • 线程一将主内存中的共享变量 A 加载到自己的本地内存中进行处理。比如 A = 1;
  • 此时将修改的共享变量 A 刷入到主内存中, 之后线程二再将主内存中的共享变量 A 读取到本地内存进行操作;

整个数据交互的过程是JMM控制的,主要控制主内存与每个线程的本地内存如何进行交互来提供共享数据的可见性

3.重排序

程序在执行的时候为了提高效率会将程序指令进行重新排序

3.1 重排序分类

  • 编译器优化重排序

编译器在不改变单线程程序语义的情况下进行语句执行顺序的优化

  • 指令集并行重排序

如果不存在数据的依赖性的话,处理器可以改变语句对应机器指令的执行顺序

  • 内存系统重排序

由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

3.2 重排序过程

以上三种重排序都会导致我们在写并发程序的时候出现内存可见性的问题。

JMM的编译器重排序规则会禁止特定类型的编译器重排序;

JMM的处理器重排序规则会要求java编译器在生成指令序列的时候插入特定的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器进行重排序

3.3 处理器重排序

由于为了避免处理器等待向内存中写入数据的延时,在处理器和内存中间加了一个缓冲区,这样处理器可以一直向缓冲区中写入数据,等到一定时间将缓冲区的数据一次性的刷入到内存中。

优点 :

  1. 处理器不同停顿,提高了处理器的运行效率
  2. 减少在向内存写入数据时的内存总线的占用

缺点 :

  1. 每个处理器上的写缓冲区只对当前处理器可见,所以就会造成内存操作的执行顺序和实际情况不符合

例如以下场景 :

在当前场景中就可能出现在处理器 A 和处理器 B 没有将它们各自的写缓冲区中的数据刷回内存中, 将内存中读取的A = 0、B = 0 进行给X和Y赋值,此时将缓冲区的数据刷入内存,导致了最后结果和实际想要的结果不一致。因为只有将缓冲区的数据刷入到了内存中才叫真正的执行

以上主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,JMM定义了以下8种操作来完成

操作 语义解析
lock(锁定) 作用于主内存的变量,把一个变量标记为一条线程独占状态
unlock(解锁) 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取) 作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use(使用) 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
assign(赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
store(存储) 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,
以便随后的write的操作
write(写入) 作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送
到主内存的变量中

如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行

操作执行流程图解:

同步规则分析

  1. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
  2. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
  3. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
  4. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
  5. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  6. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

3.4 内存屏障指令

为了解决处理器重排序导致的内存错误,java编译器在生成指令序列的适当位置插入内存屏障指令,来禁止特定类型的处理器重排序

内存屏障指令

屏障类型 指令示例 说明
LoadLoadBarriers Load1;LoadLoad;Load2 Load1数据装载发生在Load2及其所有后续数据装载之前
StoreStoreBarriers Store1;StoreStore;Store2 Store1数据刷回主存要发生在Store2及其后续所有数据刷回主存之前
LoadStoreBarriers Load1;LoadStore;Store2 Load1数据装载要发生在Store2及其后续所有数据刷回主存之前
StoreLoadBarriers Store1;StoreLoad;Load2 Store1数据刷回内存要发生在Load2及其后续所有数据装载之前

3.5 happens-before(先行规则)

happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据

在JMM中如果一个操作中的结果需要对另一个操作可见,那么这两个操作之前必须要存在happens-before关系 (两个操作可以是同一个线程也可以不是一个线程)

规则内容:

  • 程序顺序规则 : 指的是在一个线程内控制代码顺序,比如分支、循环等,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行
  • 加锁规则 : 一个解锁(unlock)操作一定要发生于一个加锁(lock)操作之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)
  • volatile变量规则 : 对一个volatile的变量的写操作要发生在对这个变量的读操作之前,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值
  • 线程启动规则 : 线程的启动方法 start() 要发生在当前线程所有操作之前
  • 线程终止规则 : 线程中所有的操作都要发生在线程终止之前,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见
  • 线程中断规则 : 线程调用interrupt()方法要发生在被中断线程的代码检查出中断事件之前
  • 对象终结规则 : 对象的初始化完成要发生在对象被回收之前
  • 传递性规则 : 如果操作 A 发生在操作 B 之前,操作 B 又发生在操作 C 之前,那么操作A一定发生于操作 C 之前

注意: 两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行,只需要前一个操作的结果对后一个操作可见,并且前一个操作按顺序要排在后一个操作之前。

3.6 数据依赖性

就是前一个操作的结果对后一个操作的结果产生影响,此时编译器和处理器在处理当前有数据依赖性的操作时不会改变存在数据依赖的两个操作的执行顺序

注意: 此时所说的数据依赖仅仅针对单个处理器中执行的指令序列或者单个线程中执行的操作。不同处理器和不同线程的情况编译器和处理器是不会考虑的

3.7 as-if-serial

单线程情况下不管怎么重排序程序的执行结果不能被改变,所以如果在单处理器或者单线程的情况下,编译器和处理器对于有数据依赖性的操作是不会进行重排序的。反之如果没有数据依赖性的操作就有可能发生指令重排。

5.数据竞争与顺序一致性

在多线程情况下才会出现数据竞争

5.1 数据竞争

在一个线程中写了一个变量,在另一个线程中读一个变量,而且写和读并没有进行同步

5.2 顺序一致性

如果在多线程条件下,程序能够正确的使用同步机制,那么程序的执行将具有顺序一致性(就像在单线程条件下执行一样) 程序最终运行的结果与你预期的结果一样

5.3 顺序一致性内存模型

5.3.1特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行
  • 所有的操作都必须是原子性的操作,并且对其他线程可见的

5.3.2概念:

在概念上,顺序一致性有一个单一的全局内存,在任意时间点最多只有一个线程可以连接到内存,当在多线程的场景下,会把所有内存的读写操作变成串行化

5.3.3案例:

例如有多个并发线程 A B C, A 线程有两个操作 A1 A2, 他们的执行的顺序是 A1 -> A2 。B 线程有三个操作 B1 B2 B3, 他们的执行的顺序是 B1 -> B2 ->B3 。C 线程有两个操作 C1 C2 那么他们在程序中执行的顺序是 C1 -> C2 。

场景分析 :

场景一 : 并发安全(同步)执行顺序

A1 -> A2 -> B1 -> B2 ->B3 -> C1 -> C2

场景二: 并发不安全(非同步)执行顺序

A1 -> B1 -> A2 -> C1 -> B2 ->B3 -> C2

结论 :

在非同步的场景下,即使三个线程中的每一个操作乱序执行,但是在每个线程中的各自操作还是保持有序的。并且所有线程都只能看到一个一致的整体执行顺序,也就是说三个线程看到的都是该顺序 : A1 -> B1 -> A2 -> C1 -> B2 ->B3 -> C2 ,因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

以上案例场景在JMM中不是这样的,未同步的程序在JMM中不仅整体的执行顺序变了,就连每个线程的看到的操作执行顺序也是不一样的

例如前面所说的如果线程A将变量的值 a = 2 写入到了自己的本地内存中,还没有刷入到主存中,在线程 A 来看值是变了,但是其他线程 B 线程 C 根本看不到值的改变,就认为线程A 的操作还没有发生,只有线程 A 将工作内存中的值刷回主内存线程 B和线程C 才能的到。但是如果是同步的情况下,顺序一致性模型和JMM模型执行的结果是一致的,但是程序的执行顺序不一定,因为在JMM中,会发生指令重排现象所以执行顺序会不一致

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

  1. 全面理解Java内存模型

    尊重原创:http://blog.csdn.net/suifeng3051/article/details/52611310 Java内存模型即JavaMemory Model,简称JMM.JMM定义 ...

  2. Java并发编程(五)-- Java内存模型补充

    前面我们已经介绍了:当对象和变量存储到计算机的各个内存区域时,必然会遇到的两个问题及解决方法 共享对象的可见性-- 解决方法:使用java volatile关键字 共享对象的竞争现象 -- 解决方法: ...

  3. Java并发编程(四)-- Java内存模型

    Java内存模型 前面讲到了Java线程之间的通信采用的是共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见.从抽象的角 ...

  4. 全面理解Java内存模型(转)

    转自:http://blog.csdn.net/suifeng3051/article/details/52611310 Java内存模型即Java Memory Model,简称JMM.JMM定义了 ...

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

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

  6. 求你了,再问你Java内存模型的时候别再给我讲堆栈方法区了…

    GitHub 4.1k Star 的Java工程师成神之路 ,不来了解一下吗? GitHub 4.1k Star 的Java工程师成神之路 ,真的不来了解一下吗? GitHub 4.1k Star 的 ...

  7. 从 CPU 讲起,深入理解 Java 内存模型!

    Java 内存模型,许多人会错误地理解成 JVM 的内存模型.但实际上,这两者是完全不同的东西.Java 内存模型定义了 Java 语言如何与内存进行交互,具体地说是 Java 语言运行时的变量,如何 ...

  8. 再问你Java内存模型的时候别再给我讲堆栈方法区

    在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情.要说计算机的内存模型,就要说一下一段古老的历史,看一下为什么要有内存模型 ...

  9. JVM学习(3)——总结Java内存模型

    俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...

随机推荐

  1. Python正则表达式re.findall一个有趣的现象

    下面通过几个案例来分析一下, 注意:本节的parsematch函数请参考<妙用re.sub分析正则表达式解析匹配过程> 案例一: >>> re.findall(r&quo ...

  2. ollvm在VS2017下编译

    0x1,首先介绍一下编译环境配置 1.UE4.25 2.vs2017(15.9),注:2019编译总是出现错误 3.cmake3.18.5,cmake的作用是为ollvm源码编译成适合于在vs2017 ...

  3. PHP代码审计分段讲解(3)

    05 ereg正则%00截断 放上源代码 <?php $flag = "flag"; if (isset ($_GET['password'])) { if (ereg (& ...

  4. C#使用ML.Net完成人工智能预测

    前言 Visual Studio2019 Preview中提供了图形界面的ML.Net,所以,只要我们安装Visual Studio2019 Preview就能简单的使用ML.Net了,因为我的电脑已 ...

  5. 对网页接口的追踪探索(以b站通过bv号查询av号为例

    对网页接口的追踪探索(以b站通过bv号查询av号为例 序言 本文只提供一种探索网页加载时后端访问接口情况的思路,所举例子没有太大实际用处. 一 自2020年3月23日起,AV号将全面升级到BV号.但是 ...

  6. es6语法糖

    ES6为一些已有的功能提供了非破坏性更新,这类新语法能做的事情其实用ES5也可以做,只是会稍微复杂一些,称之为语法糖. 对象属性的简洁表示法 声明的对象中包含若干属性,其属性值由变量表示,且变量名和属 ...

  7. js原生方法promise的实现

    一会儿就要回家过年了,再来手写一个promise吧,要不等着下班真的煎熬... <!DOCTYPE html> <html lang="en"> <h ...

  8. 【题单】最近遇见的 SHIT DP题 三连

    Hint: 本题单适合用于自虐和消磨时间. CF-Gym101620E https://codeforces.com/gym/101620 ARC109F https://atcoder.jp/con ...

  9. 题解-ARC058D Iroha Loves Strings

    题面 ARC058D Iroha Loves Strings 给定 \(n\) 个字符串,从中选出若干个按给出顺序连接起来,总长等于 \(m\),求字典序最小的,保证有解. 数据范围:\(1\le n ...

  10. NOI 2020 D1T3 本人题解

    我看了出题人本题的做法,感觉很难写,就自己胡了一个\(O((n + m) \sqrt n)\)的做法. 第一步我的想法与出题人一样,都是考虑容斥降维.对第\(i\)组询问,我们枚举两个事件中较大的一个 ...