1.JAVA 的并发模型

共享内存模型

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

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

2.JAVA 内存模型的抽象

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

非线程安全 : 在 java 中所有的实例域、静态域、和数组元素都存放在堆内存中,并且这些数据是线程共享的,所以会存在内存可见性问题

线程安全 : 局部变量、方法定义的参数、异常处理器参数是当前线程的虚拟机栈中的数据,并且不会进行线程共享,所以不会存在内存可见性问题

2.2 线程间通讯的本质

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

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

消息通讯过程(不考虑数据安全性的问题) :

线程一将主内存中的共享变量 A 加载到自己的本地内存中进行处理。比如 A = 1;

此时将修改的共享变量 A 刷入到主内存中, 之后线程二再将主内存中的共享变量 A 读取到本地内存进行操作;

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

3.重排序

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

3.1 重排序分类

编译器优化重排序

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

指令集并行重排序

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

内存系统重排序

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

3.2 重排序过程

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

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

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

3.3 处理器重排序

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

优点 :

处理器不同停顿,提高了处理器的运行效率

减少在向内存写入数据时的内存总线的占用

缺点 :

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

例如以下场景 :

在当前场景中就可能出现在处理器 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内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行

操作执行流程图解:

同步规则分析

不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中

一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。

一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。

如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。

如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

3.4 内存屏障指令

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

内存屏障指令

屏障类型指令示例说明LoadLoadBarriersLoad1;LoadLoad;Load2Load1数据装载发生在Load2及其所有后续数据装载之前

StoreStoreBarriersStore1;StoreStore;Store2Store1数据刷回主存要发生在Store2及其后续所有数据刷回主存之前

LoadStoreBarriersLoad1;LoadStore;Store2Load1数据装载要发生在Store2及其后续所有数据刷回主存之前

StoreLoadBarriersStore1;StoreLoad;Load2Store1数据刷回内存要发生在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 内存屏障类型_Java内存模型精讲相关推荐

  1. 内存屏障(cpu内存屏障 与java内存屏障)

    文章目录 CPU 内存屏障 定义 读写屏障指令 为什么会出现内存屏障 java内存屏障 java内存屏障存在意义 java中内存屏障的主要类型 LoadLoad 屏障 StoreStore 屏障 Lo ...

  2. java volatile内存屏障_volatile 和 内存屏障

    接下来看看volatile是如何解决上面两个问题的: 被volatile修饰的变量在编译成字节码文件时会多个lock指令,该指令在执行过程中会生成相应的内存屏障,以此来解决可见性跟重排序的问题. 内存 ...

  3. 【NLP】一份相当全面的BERT模型精讲

    本文概览: 1. Autoregressive语言模型与Autoencoder语言模型 1.1 语言模型概念介绍 Autoregressive语言模型:指的是依据前面(或后面)出现的单词来预测当前时刻 ...

  4. java和硬件交互_Java内存模型

    Java内存模型 我们常说的JVM内存模式指的是JVM的内存分区:而Java内存模式是一种虚拟机规范,真实并不存在 Java虚拟机规范中定义了Java内存模型(Java Memory Model,JM ...

  5. 浅谈内存屏障,C++内存序与内存模型

    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可. 本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权. 文章目录 引言 一个有意思的问题 ...

  6. java内存分析工具_java内存查看与分析

    业界有很多强大的java profile的工具,比如Jporfiler,yourkit,这些收费的东西我就不想说了,想说的是,其实java自己就提供了很多内存监控的小工具,下面列举的工具只是一小部分, ...

  7. java内存区域与内存溢出异常_Java内存区域与内存溢出异常

    Java的内存管理是一个老生常谈的问题,虽然Java号称可以自动管理自己的内存,使程序员从内存管理的围墙解放出来,但是一连串的内存泄漏和溢出方面的问题,使得我们不得不去深入了解Java的内存管理机制. ...

  8. java 内存详解_Java内存详解

    一.了解java的内存大致划分 栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中. 堆:存放用new产生的数据 静态域:存放在对象中用static定义的静态成员 常量池:存放 ...

  9. java double储存原理_Java内存分配原理

    Java内存分配与管理是Java的核心技术之一,一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中 ...

最新文章

  1. 漫谈天际网的用户体验
  2. UI组件之ImageView及其子类(一)ImageView显示图片
  3. python2卸载后yum不可用_centos7误删除python2导致的python和yum不可用处理-阿里云开发者社区...
  4. thinkphp5.0解决控制器驼峰命名时提示找不到类名
  5. 在linux下编译android下的opencv,使用cmake的方法
  6. oracle读写文件--利用utl_file包对磁盘文件的读写操作
  7. 视频教程-java后台+微信小程序 实现完整的点餐系统-微信开发
  8. 基于linux 的 PCI PCIe 总线分析总结
  9. android平台串口调试工具,串口调试助手下载-串口助手 安卓版v1.4-PC6安卓网
  10. HTML5期末大作业:漫画网站设计——海贼王基地(5页) 学生动漫网页设计模板下载 海贼王大学生HTML网页制作作品 简单漫画网页设计成品 dreamweaver学生网站模板
  11. 微信小程序写动画 一闪一闪亮晶晶
  12. eclipse报 The word is not correctly spelled问题
  13. Excel 表格图片压缩方法
  14. 《事情正在起变化》《这是为什么》?
  15. 数字图像处理--冈萨雷斯第4版--第一章 绪论
  16. python-基础语法-glob.glob()
  17. Linux 存储 Stack详解
  18. xmind-8 安装以及 如何激活
  19. Xshell安装和使用教程——小白必看
  20. 通胀叙事下 美国难以重现沃尔克时代

热门文章

  1. oracle参数文件initorcl位置,oracle 参数文件详解
  2. [LeetCode] Plus One - 整数字符转换相加
  3. [Android] SQLite数据库之增删改查基础操作
  4. 【数据结构与算法】之深入解析“键盘行”的求解思路与算法示例
  5. 【数据结构与算法】之深入解析“最小栈”的求解思路与算法示例
  6. 133. Clone Graph 克隆图
  7. ZYAR20A 亚克力2驱 蓝牙 298寻迹避障机器人 —— 小车超声波避障实验(无舵机)
  8. 28335的CPU定时器解析
  9. 【Tools】Visual Studio 2019下载和安装
  10. 【Linux】一步一步学Linux——chmod命令(110)