JMM
JMM
MR.XSSJMM
JMM内存模型
Java Memory Model
java内存模型,用来屏蔽操作系统和各种硬件的内存访问差异,以实现Java程序在各种平台下运行都能达到一致的内存访问效果
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
注意:这里所说的主存和计算硬件主存相似,但是java的这片区域是虚拟机的一部分
JMM定义了什么
JMM围绕了三个特征建立起来的,分别是原子性、可见性、有序性,这三个特征是java并发的基础
原子性
定义:原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。
1 | int i = 0; |
第一个操作是原子性的,基本复制语句;
第二个,多个线程会发生竞态,Check then Act 先检查后执行,i++是四个字节码指令,先读取i值,然后再对+1操作,再将i写回到内存中
第三个和第二个一样,都不是原子操作
可见性
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。
除了volatile关键字之外,final和synchronized也能实现可见性。
synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。
final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。
案例
1 | package Thread.Day04; |
结果
出现这种结果和java的内存模型有关当多次对一个变量进行读取的时候,java会将数据读取到高速缓存区,这样就会导致变量的不可见,当外部对该变量进行修改的时候,线程内部是无法看到的,导致了运行出现了死循环。
分析
初始状态,t线程刚开始从主内存读取了run的值到工作内存。
因为t线程要频繁地从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率。
1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
解决方案
- 使用volatile关键字可以时共享变量保持可见性
1 | package Thread.Day04; |
- 使用synchronized同步,同样也可以维持正常
结果
synchronized和volatile对比
synchronized
用于修饰方法或者代码块,volatile
只能修饰变量。synchronized
保证操作的原子性,同时保证变量的可见性,volatile
保持变量的可见性。synchronized
通常适用于写多读少的场景,会造成线程阻塞,volatile
通常适用于写少读多的场景,不会造成线程阻塞。
有序性
指令重排序
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
使用volatile关键字和synchronized关键字可以保证不会指令重排
volatile关键字,实现禁止重排序原理是实现了内存读写屏障
volatile原理
- 写屏障保证在该屏障之前的,对共享变量进行的改动,都会同步到主存当中
- 而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据