Open liu1813565583 opened 5 years ago
导致可见性的原因是缓存,导致有序性的原因是编译优化,那么解决可见性、有序性的最直接办法就是禁用缓存和编译优化,但如果这么做,我们程序的性能就堪忧了。
合理的方案应该是按需「即按照程序猿的需求」禁用缓存和编译优化。Java 提供了方法来让程序员禁用缓存和编译优化
这里引出了 Java 内存的概念,即内存模型规范了JVM 如何按需禁用缓存和编译优化的方法,包括:
声明一个 volatile 变量 volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。
大家都说了很多了,我简单补充下吧。
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变量,来禁用指令重排导致的这种问题。
为什么会有Java内存模型?
并发程序里,当多个线程同时访问同一个共享变量的时候,结果是不确定的。不确定,则意味着可能正确,也可能错误,事先是不知道的。而导致不确定的主要源头是可见性问题、有序性问题和原子性问题,为了解决这三个问题,Java 语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则,我们可以避免可见性问题、有序性问题。
Java内存模型是什么?
JMM决定了一个线程对共享变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。
计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,用户磁盘和CPU的交互,而CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用户缓冲用户IO等待导致CPU的等待成本,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状。同样,根据摩尔定律,我们知道单核CPU的主频不可能无限制的增长,要想很多的提升新能,需要多个处理器协同工作, 基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。为了解决这一问题,需要各个处理器运行时都遵循一些协议,在运行时需要将这些协议保证数据的一致性。这类协议包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。
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文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
楼上的大佬们都差不多总结完了,我只是边跟着学习边更新:https://www.jianshu.com/p/6ec4b36bdd72
引言
Java 内存模型 , 即 Java Memory Model,JMM 来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java 在各种平台都达到一致的访问效果.Java 内存模型规范了 JVM 如何 禁用缓存 和 编译优化 的方法.
一.Main Memory和Working Memory
下面我就用图文并茂的方式给大家展示一下: Thread,Main Memory和Working Memory的交互关系
二.内存间交互操作
jvm实现必须保证每一种操作都是原子的,不可细分的(double 和 long比较特殊)
Java 提出了 8种操作类型
变量作用域: Main Memory
作用域: Working Memory
变量作用域: Main Memory
操作规则:
太官方,主要是为了解决Java并发安全,Android用的不太多,只想理清流程,不想看细节~
三.volatile
没有被volatile位数据可以分割为两个32位进行操作,volatile和syhnronized功能类似,解决多Thread竞争问题
操作规则:
四.long和double
long 和 double 具有非原子性,不需要用考虑使用volatile修饰
五.原子性,可见性,有序性
原子性: 除了 long 和 double以外的基本数据类型都具有原子性
可见性: 当一个线程修改了这个值,其他线程能够立即感知,即 Main Memory 传递的方式具有可见性
volatile 能保证新值能立即同步到 Main Memory,以及每次使用 从 Main Memory 立即 refresh.
final 字段在constrctor方法 init 后,constrctor 没有把 "this" 引用传递出去(this 逃逸非常危险),那么在其他线程中就能看见 final 字段的值
synchronized 变量在同步代码块,unlock之后,必须回到 Main Memory,执行完write操作
有序性:
ThreadA watch ThreadB 都是无序的(指令重排,Main Memory与Working Memory 同步 延迟)
volatile 禁止指令重排
synchronized 一个变量 一个时刻只允许一条线程进行lock
六.happen-before
程序次序规则
在一个程序里面,按照程序代码顺序,书写在前面的操作happen-before发生书写在后面的操作
管程锁定规则
unlock happen-before 同一把锁的lock 操作
线程中断原则
Thread.intercupt()方法检测到是否中断发生
传递性
Happens-Before 具有传递性,如果 A Happens-Before B,B Happens-Before C,则 A Happens-Before C
对象终结规则
对象 init happen-before finalize()