目录
除此之外,java语言中有一个先行发生原则( happens-before ) 来保证JMM的有序性。
文章内容
Java虚拟机规范试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。因此java虚拟机类比现代计算机内存模型规范了java内存模型JMM。同样会出现缓存不一致问题和指令重排序的问题?那java如何来解决的呢?为何原子性,可见性,有序性是java并发编程3大特性呢?看下去应该会有收获!!
JMM(Java内存模型)
Java内存模型规定所有的变量( 实例变量和静态变量, 但不包括局部变量) 都是存在主内存当中( 相当于计算机的物理内存 ), 每个线程(因为线程跑在jvm中,而jvm可以理解成是一个虚拟的系统) 都有自己的工作内存( 类似于计算机模型的高速缓存 )。
线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作操作主内存。并且每个线程不能访问其他线程的工作内存。
JMM原理图
原子性
从数据类型方面:int等不大于32位的基本类型的操作都是具有原子性。
从操作语句方面:原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
分析下列语句的是否为原子操作
i =666;原子性, 线程执行这个语句时,直接将数值666写入到工作内存中。
i = j; 看起来也是原子性的,但是它实际上涉及两个操作,先去读j的值,再把j的值写入工作内存,两个操作分开都是原子操作,但是合起来就不满足原子性了。
i = i+1;i++; 都是非原子性。
要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。volatile是无法保证复合操作的原子性。
可见性
当一个线程修改了共享变量的值时,其他线程能够立即感知到这个修改。(JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此)。
Java语言通过锁、synchronized,volatile,final都可以实现可见性。
有序性
JMM定义: 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中,观察另一个线程,所有的操作都是无序的,后半句的意思即 允许编译器和处理器对指令进行重排序, 会影响到多线程并发执行的正确性。而前半句意思就是as-if-serial的语义,即不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会被改变。
Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。
volatile底层是如何保证可见性和禁止指令重排的呢?
实际上volatile保证可见性和禁止指令重排都跟内存屏障有关
volatile关键字加上变量前,当变量的值更改则立即强制同步到主内存,并且通知其他线程操作被volatile修饰的变量时要重主内存重新读。这就保证了可见性。
了解volatile底层如何解决指令重排首先了解什么是指令重排。
指令重排是指在程序执行过程中,为了提高性能, 编译器和CPU可能会对指令进行重新排序。
下图重排序类型和重排序执行过程:
JVM解决指令重排问题用到内存屏障
而为了实现volatile的内存语义,Java内存模型采取以下的保守策略
1. 在每个volatile写操作的前面插入一个StoreStore屏障
2. 在每个volatile写操作的后面插入一个StoreLoad屏障
3. 在每个volatile读操作的前面插入一个LoadLoad屏障
4. 在每个volatile读操作的后面插入一个LoadStore屏障
除此之外,java语言中有一个先行发生原则( happens-before ) 来保证JMM的有序性。
1. 程序次序规则
在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
2. 管程锁定规则
一个unLock操作先行发生于后面对同一个锁额lock操作。
3. volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作。
4. 线程启动规则
Thread对象的start()方法先行发生于此线程的每个一个动作。
5. 线程终止规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
6. 线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7. 对象终结规则
一个对象的初始化完成先行发生于他的finalize()方法的开始。
8. 传递性
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
总结volatile的内存语义
1. 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存 ( 强制刷新)。
2. 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量 (强制过期)。