Java并发之volatile详解

1、为什么需要volatile,volatile能解决什么问题

2、volatile的实现原理是什么

3、什么是happen-before

4、volatile是否能保证线程安全

Java内存模型JMM

介绍volatile之前,首先对Java内存模型进行说明。在C\C++等语言中,内存管理直接使用物理硬件和操作系统的内存模型,也因此会导致程序无法在不同平台上完全兼容。《Java虚拟机规范》中试图定义Java内存模型(Java Memeroy Model)来屏蔽硬件和操作系统之间的内存访问差异,以达到Java程序的跨平台兼容性。

定义Java内存模型并非易事,模型必须定义严谨,不能让内存访问产生歧义;也必须足够宽松,便于虚拟机有足够的灵活度去利用硬件的特性来提升内存操作速度。经过长时间验证、修补,直到Jdk 5,Java内存模型才成熟起来。

JMM

Java内存模型规定所有变量都存储在主内存中(虚拟内存,非物理内存),每条线程都有自己的工作内存,工作内存中拷贝了线程所需变量的副本。线程对所有变量的操作都作用在工作内存的副本上,不能直接操作主内存。不同线程之间的工作内存互相隔离,无法直接访问其他工作内存中的变量。

volatile作用介绍

volatile是Java提供的最轻量级的同步操作,用于保障可见性和有序性

可见性

在JMM的介绍中可知,每个Java线程都拥有自己的工作内存,如果两个线程共享同一个变量, 那么每个线程都在自己的工作内存中拷贝了一份该变量。当A线程对变量做出修改后,B线程对变量的修改是不能立即可见的,只有当A线程将变量刷入主内存,并在B线程重新加载主内存变量时,B线程才能得到A线程修改的值。

变量添加volatile后,即可让修改立即同步到主存中,并要求在使用前立即从主内存重新读取,以保证变量的可见性。

synchronized释放锁后,同步块的修改都会同步到主存,因此synchronized也可保证可见性。

有序性

Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指"线程内似表现为串行的寓意",后半句是指"指令重排"和"工作内存与主内存同步延迟"现象。

public class Singleton { private static volatile instance; private Singleton() {} public stativ Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

以标准的单例模式为例,创建单例对象在执行时实际分为三步:分配内存空间、初始化对象、将内存地址赋值给引用;在指令重排后可能会变为:分配空间、赋值引用、初始化对象。

考虑多线程环境,A线程执行了new Instance()赋值引用且尚未初始化,B线程进入方法判断instance == null为false,直接获取到一个未初始化的对象,可能引起一些不可预料的错误。通过添加volatile可确保insntace的创建过程中不被重排序。

synchronized由于同一时刻只允许一个线程进入代码块,因此synchronized也可保证有序性。

原子性

volatile只能保证读写的原子性,无法保证其他操作的原子性。

volatile int i = 0; for (int n = 0; n < 1000; n++) { new Thread(() -> i++).start(); }

如示例代码,常见的误区是i在多线程自增到1000,实际上volatile并不能保证i++的同步和原子性。因为i++会分解为三条指令:读取、加1、写入,volatile只有在读取和写入阶段可保证原子性,因此想要解决同步问题,还是要依靠synchronized和lock。

volatile原理分析 可见性原理 public class Test { private volatile int a; public void update() { a = 1; } public static void main(String[] args) { Test test = new Test(); test.update(); } }

通过hsdis和jitwatch查看编译后的汇编代码。

.... 0x000000000295158c: lock cmpxchg %rdi,(%rdx) // volatile写增加lock前缀 .... lock指令和缓存一致性协议

lock前缀指令的执行会触发两件事:

将当前处理器缓存行的数据写回到主存

将其他处理缓存行内的该内存地址的缓存失效

假设A、B两个线程都将变量var的指加载到了自己的工作内存,对var添加volatile修饰后,当A线程修改var的值,该修改会立即刷新到主内存中,并且B线程的var变量缓存会置为失效,当B线程读取该变量时,就需要重新到主内存中加载,如此便保持了变量的可见性。这种多缓存场景下的数据一致性通过缓存一致性协议(MESI)保证。

关于缓存一致性推荐参考第三篇文章,附MESI演示。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzdfwy.html