什么是Volatile?
Volatile是Java虚拟机提供的轻量级的同步机制。也可以视为synchronized的简易版。
Volatile的三大特性
- 保证可见性
- 不保证原子性
- 禁止指令重排
什么是可见性?
首先了解一个概念:由于JVM运行程序的实体是线程,每个线程在创建的时候JVM都会为其创建一个各自独立的工作内存,也就是说每个线程都会有自己独立的一个私密空间,而JMM规定所有变量都要存储在主内存,主内存是共享内存区域,所有线程都能访问,但线程对变量的读取操作又都必须在工作内存,首先将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回到主内存。不能直接在主内存中操作变量,各个线程的工作内存中都存储着主内存变量的拷贝副本,线程之间的数据不共享,所以线程之间的传值必须通过主内存来完成。
因此:在多线程的情况下,假如一个线程修改了主内存的值,其他线程马上就能知道,即为可见性。
什么是原子性?
原子性说白了就是完整性。即当一个线程在执行具体的操作时,期间不可以被打断,要一气呵成,整体完成。
如何解决Volatile不保证原子性的问题?
- 加同步锁:synchronized。可以,但是如果变量仅仅是基本数据类型的,例如:int。此时若用 synchronized 可谓是杀鸡用牛刀,大材小用了。
- 使用JUC (java.util.concurrent) 包下的 AtomicInteger 替代 int 。
为什么“加个Atomic”就能保证原子性呢?
Atomic修饰的数据类型底层原理是:CAS(Compare and Swap)比较并交换。里面涉及到Unsave类和自旋锁。
什么是指令重排?
计算机在执行程序时,为了提高性能,编译器和处理器经常会对指令执行的顺序进行重排,重排一般分为三种
- 编译器优化的重排
- 指令并行的重排
- 内存系统的重排
单线程环境可以保证程序最终执行的结果和代码顺序执行的结果保持一致
处理器在指令重排时必须考虑指令之间的数据依赖性,例如:
int x = 1; //1
int y = 2; //2
x = x + 3; //3
y = x * x; //4
//这段代码执行的顺序正常来讲是1234,但涉及到指令重排,顺序又有可能是1324,但绝不可能是3124和4123
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,因此结果也是无法预测的,例如:
int a,b,x,y = 0;
线程1 线程2
x = a; y = b;
b = 1; a = 2;
结果 x = 0; y = 0;
abxy初始值都是0,正常顺序执行后x = 0; y = 0;
- - - - - - -- - -- - - - - - - - - -- - -- -- -- - -
- - - - - - - - - - - - - -- - - - - - - - - - -- - - - -
但如果指令重排序,如:
线程1 线程2
b = 1; a = 2;
x = a; y = b;
结果 x = 2; y = 1;
此时指令b = 1 和a = 2 先执行,结果便会有所影响
volatile如何禁止指令重排?
volatile通过内存屏障 ( Memory Barrier ) 实现禁止指令重排优化,从而避免多线程环境下指令乱序执行的现象。
内存屏障,又叫内存栅栏,是一个CPU指令,有两个作用:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能对指令进行重排优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和CPU,不管什么指令都不能和这条指令重排序,即:通过插入内存屏障禁止在内存屏障前后的指令之间进行指令重排。内存屏障的另外一个作用就是强制刷出各种CPU的缓存数据,使任何线程都能读取到CPU上数据的最新版本。
Volatile使用场景——>单例模式