这是我参与更文挑战的第9天,活动详情查看: 更文挑战
一、CPU cache模型和JMM模型(Java memory mode)
要想知道volatile关键字的作用,我们得首先清楚,硬件cpu缓存模型和java内存模型的关系:
1.Cpu cache 模型:
缓存速度:寄存器>L1>L2>L3>主内存。主内存中放入的是共享的一些数据,cpu从主内存中读取数据到三级缓存再到寄存器修改,之后把修改后的数据刷新到主内存中。这个时候我们思考,如果是双CPU,都要修改同一个数据,是不是会引发数据安全的问题呢?通常的解决办法是通过总线BUS或EMSI协议,总线BUS的粒度要比EMSI大,耗费的资源高。
简单解释一下EMSI协议:E(Exclusive 独有的)、M(Modified 修改的)、S(Share 共享的)、I(Invalid 无效的);各个缓存中有一个叫Cache line的有这四种状态,当Cache line为E和S的状态,缓存中的数据和主内存的数据是一致的,是clean的数据、当Cache line为独有时表示,数据只在当前的缓存中存在,Cache line 为共享的时候,表示在别的缓存中也存在。当Cache line 为M的状态,表示在本地缓存中已经修改,其他缓存会有一个监听机制,Cache line会变成I无效。当修改数据刷新到主内存后,其他线程的Cache line又会变为E或者S。
2.Java内存模型
首先声明java内存模型是一个抽象的概念,它并不在物理空间上存在。它的设计思想和CPU cache模型类似,如果要修改一个数据,线程会先到工作空间去找有没有该数据,如果没有就去主内存中找,读到工作空间后,再修改值,再刷新到主内存中。当然,聪明的你会想到数据安全的问题。
3.java内存模型和cpu cache模型的关系:
工作空间和主内存的数据都可能来自Cpu cache模型中寄存器,缓存,主内存。
二、volatile的作用:
1.volatile是用来修饰变量的:
private static volatile int init_value = 0;
volatile具有防止指令重排的作用,他属于happens before原则中的一种。简单来说,jvm会对我们写的代码进行优化,代码的执行顺序可能会被打乱,加了volatile,在volatile之前的变量和之后的变量执行顺序都不能被改变,因此他具有有序性。此外他还保证了数据的可见性,当一个线程修改了某个数据时,其他线程会嗅探到这个数据被改变了,也就是说这个数据被改变了对其他线程可见。
我们可以通过hsdis和jitwatch得到汇编代码:
......
0x0000000002951563: and $0xffffffffffffff87,%rdi
0x0000000002951567: je 0x00000000029515f8
0x000000000295156d: test $0x7,%rdi
0x0000000002951574: jne 0x00000000029515bd
0x0000000002951576: test $0x300,%rdi
0x000000000295157d: jne 0x000000000295159c
0x000000000295157f: and $0x37f,%rax
0x0000000002951586: mov %rax,%rdi
0x0000000002951589: or %r15,%rdi
0x000000000295158c: lock cmpxchg %rdi,(%rdx) //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
0x0000000002951591: jne 0x0000000002951a15
0x0000000002951597: jmpq 0x00000000029515f8
0x000000000295159c: mov 0x8(%rdx),%edi
0x000000000295159f: shl $0x3,%rdi
0x00000000029515a3: mov 0xa8(%rdi),%rdi
0x00000000029515aa: or %r15,%rdi
......
三、volatile的运用场景:
1.利用可见性做读写分离:
//volatile保证了数据的可见性
public class VolatileDemo {
private static final int MAX = 5;
private static volatile int init_value = 0;
public static void main(String[] args) {
//读
new Thread(()->{
int localValue = init_value;
while (localValue<MAX){
if (localValue != init_value){
System.out.printf("init_value is update to %d \n",init_value);
localValue = init_value;
}
}
},"Reader").start();
//写
new Thread(()->{
int localValue = init_value;
while (localValue<MAX){
System.out.printf("init_value will change to %d \n",(++init_value));
localValue =init_value;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Writer").start();
}
}
结果如下:
init_value will change to 1
init_value is update to 1
init_value will change to 2
init_value is update to 2
init_value will change to 3
init_value is update to 3
init_value will change to 4
init_value is update to 4
init_value will change to 5
init_value is update to 5
2.利用可见性做开关
//volatile的使用场景2,做开关用
public class ShutdownDemo extends Thread{
private volatile static boolean flag = true;
@Override
public void run() {
doWork();
}
private void doWork() {
while (flag){
System.out.println("I am working.....");
}
}
private void shutDown(){
try {
TimeUnit.SECONDS.sleep(2);
flag = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ShutdownDemo shutdownDemo = new ShutdownDemo();
//这里运行的是run方法
new Thread(shutdownDemo).start();
new Thread(shutdownDemo::shutDown).start();
}
运行结果:2s后停止工作
I am working.....
I am working.....
I am working.....
I am working.....
I am working.....
Process finished with exit code 0
使用场景 3:DCL(double check lock)单例模式
public class DCLDemo {
private volatile static DCLDemo dclDemo;
private static DCLDemo getDclDemo(){
if (dclDemo==null){
synchronized (DCLDemo.class){
if (dclDemo==null){
dclDemo = new DCLDemo();
}
}
}
return dclDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()-> System.out.println(getDclDemo())).start();
}
}
}
PS:优雅的懒加载:(静态内部类,当然还有枚举等方式也不错)
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
四、volatile的缺点:
无法保证原子性,要保证原子性,请看我写的另外一篇文章,synchronize的理解:
//volatile关键字并不能保证原子性
public class VolatileIncreaseDemo {
private static volatile int i =0;
private static void increase(){
i++;
}
public static void main(String[] args) {
for (int j = 0; j < 10; j++) {
new Thread(()->{
for (int k = 0; k < 5; k++) {
increase();
}
}).start();
}
System.out.println(i);
}
}
运行结果是小于50的:
20
Process finished with exit code 0