MicroKibaco / CrazyDailyQuestion

每日一问: 水滴石穿,聚沙成塔,坚持数月, 必有收获~
35 stars 1 forks source link

2019-08-08:谈谈你对java内存模型的理解? #8

Open liu1813565583 opened 5 years ago

MicroKibaco commented 5 years ago

引言

  Java 内存模型 , 即 Java Memory Model,JMM 来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java 在各种平台都达到一致的访问效果.Java 内存模型规范了 JVM 如何 禁用缓存编译优化 的方法.

一.Main Memory和Working Memory

下面我就用图文并茂的方式给大家展示一下: Thread,Main Memory和Working Memory的交互关系

二.内存间交互操作

那么,变量是如何通过Main Memory copy给 Working Memory,如何从 Work Memory sync 到 Main Memory的呢?

jvm实现必须保证每一种操作都是原子的,不可细分的(double 和 long比较特殊)

Java 提出了 8种操作类型

变量作用域: Main Memory

方法 作用
lock(🔒) 把Thread 设置线程独有 Tag
unlock(🔓) 释放变量,给其他Thread使用
read(读取) 变量从Main Memory传输到Thread 的 Working Memory途径

作用域: Working Memory

方法 作用
load(载入) 变量值放到Working Memory 的变量副本中
use(使用) Working Memory的变量值传给执行引擎,JVM收到需要执行的变量的字节码指令时候,会执行这个操作
assign(赋值) 他把一个从执行引擎接收的值赋给Working Memory 的变量,JVM收到需要执行的变量的字节码指令时候,会执行这个操作
store(存储) 把变量值传给 Main Memory

变量作用域: Main Memory

方法 作用
write(写入) Working Memory的变量放到Main Memory中

操作规则:

  太官方,主要是为了解决Java并发安全,Android用的不太多,只想理清流程,不想看细节~

三.volatile

  没有被volatile位数据可以分割为两个32位进行操作,volatile和syhnronized功能类似,解决多Thread竞争问题

操作规则:

四.long和double

  long 和 double 具有非原子性,不需要用考虑使用volatile修饰

五.原子性,可见性,有序性

六.happen-before

ArtarisCN commented 5 years ago

Java内存模型:看Java如何解决可见性和有序性问题

导致可见性的原因是缓存,导致有序性的原因是编译优化,那么解决可见性、有序性的最直接办法就是禁用缓存和编译优化,但如果这么做,我们程序的性能就堪忧了。

合理的方案应该是按需「即按照程序猿的需求」禁用缓存和编译优化。Java 提供了方法来让程序员禁用缓存和编译优化

这里引出了 Java 内存的概念,即内存模型规范了JVM 如何按需禁用缓存和编译优化的方法,包括:

volatile

声明一个 volatile 变量 volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。

Happens-Before 规则

  1. 程序的顺序性规范 按照程序执行的顺序,前面的 Happens-Before 于后续的任意的操作。程序前面对某个变量的操作一定对于后续操作可见的
  2. volatile 变量规则 对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
  3. 传递性 Happens-Before 具有传递性,如果 A Happens-Before B,B Happens-Before C
  4. 锁的规则 一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
  5. 线程 start() 规则 线程 A 启动子线程 B 后,子线程 B 能够看到线程 A 在启动子线程 B 前的操作。
  6. 线程 join() 规则 主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。

问题导致的原因

  1. 缓存导致的可见性问题
  2. 切换线程导致的原子性问题
  3. 编译优化带来的有序性问题「指令重排」
liyilin-jack commented 5 years ago

大家都说了很多了,我简单补充下吧。

java内存模型在硬件上来看,其实不止关乎“内存”,还关乎cpu。一个程序的完整执行流程,要经过cpu->内存->IO设备。

在多核cpu环境下,多个cpu都有单独的缓存,也就是说多个线程对于共享变量的访问,不具有一致性,那么怎么才能保证一直性的要求呢?就是禁用cpu缓存,直接从内存中取数据,volatile 关键子就是用来处理可见性问题的,告诉编译器,对于volatile声明的变量,不要cpu进行缓存。 这就是cpu缓存带来的可见性问题及解决方案。

针对有序性问题,是编译器在编译过程中,会对一些指令顺序进行重排,来达到最优的执行效率。 拿我们最常用的双重检查单例实现来说,当多个线程调用getInstance()时,程序执行 if(instance==null) instance = new Instance() ;这行代码看起来的执行顺序应该是: 1.创建内存 2.在内存上创建Instance对象 3.把内存地址指向instance变量

但cpu可能优化为1,3,2的顺序,导致如果在3完成后发生线程切换,程序会判断对象不为空,但实际却是空的现象。解决方案还是使用volatile来声明Instance变量,来禁用指令重排导致的这种问题。

skylarliuu commented 5 years ago

为什么会有Java内存模型?

并发程序里,当多个线程同时访问同一个共享变量的时候,结果是不确定的。不确定,则意味着可能正确,也可能错误,事先是不知道的。而导致不确定的主要源头是可见性问题、有序性问题和原子性问题,为了解决这三个问题,Java 语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则,我们可以避免可见性问题、有序性问题。

Java内存模型是什么?

JMM决定了一个线程对共享变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。

liu1813565583 commented 5 years ago

定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

chengying1216 commented 5 years ago

计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,用户磁盘和CPU的交互,而CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用户缓冲用户IO等待导致CPU的等待成本,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状。同样,根据摩尔定律,我们知道单核CPU的主频不可能无限制的增长,要想很多的提升新能,需要多个处理器协同工作, 基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。为了解决这一问题,需要各个处理器运行时都遵循一些协议,在运行时需要将这些协议保证数据的一致性。这类协议包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。

zhengjunke commented 5 years ago

Java内存模型如图所示图片 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。再虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。如哦线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。 本地方法栈与虚拟机栈所发挥的作用时非常相似的,他们之前的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 Java堆:对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的是存放对象实例。Java堆也是垃圾收集器管理的主要区域,如果堆中没有内存完成实例分配,并且堆无法再扩展时,将会抛出OutOfMemoryError异常。 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码等数据。该区域内存回收目标主要是针对常量池的回收和类型的卸载。 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只有编译器才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

pioneerz commented 5 years ago

楼上的大佬们都差不多总结完了,我只是边跟着学习边更新:https://www.jianshu.com/p/6ec4b36bdd72