1、Java的内存模型(JMM)
在仔细讲解Java的volatile关键字之前有必要先了解一下【Java的内存模型】
Java的内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范用来屏蔽【不同硬件】和【操作系统】的【内存访问差异】。
让Java程序在各种平台下都能达到一致的内存访问效果。
Java内存模型图片
主内存
主内存可以简单理解为计算机当中的内存,但又不完全等同。
主内存被所有线程共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的"本尊"。
工作内存
工作内存可以理解为计算机当中的CPU高速缓存,但又不完全等同。
每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的"副本"。
线程对【共享变量】的所有操作都必须在【工作内存】中进行,不能直接读写【主内存】中的变量。
不同线程之间也无法访问彼此的【工作内存】,【变量值的传递】只能通过【主内存】来进行。
直接操作【主内存】太慢,所以JVM才不得不利用性能较高的【工作内存】。
【工作内存】所更新的【变量】并不会立即同步到主内存。
2.volatile内存语义
volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用
- 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
- 禁止指令重排序优化。
volatile的可见性
volatile可见性是通过汇编加上Lock前缀指令,触发底层的MESI缓存一致性协议来实现的。MESI表示四种状态,如下所示:
volatile的有序性
指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序。volatile静止了指令的重排,保证了有序性。
内存屏障
volatile有序性是通过内存屏障实现的。JVM和CPU都会对指令做重排优化,所以在指令间插入一个屏障点,就告诉JVM和CPU,不能进行重排优化。具体的会分为读读、读写、写读、写写屏障这四种,同时它也会有一些插入屏障点的策略,下面是JMM基于保守策略的内存屏障点插入策略:
volatile无法保证原子性
//示例
public class VolatileVisibility {
public static volatile int i =0;
public static void increase(){
i++;
}
}
在并发场景下,i变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时调用increase()方法的话,就会出现线程安全问题,毕竟i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。