volatile关键字的作用
首先需要介绍一下并发的三个属性。
原子性
- 为什么叫原子性呢,因为原子就是不可拆分的了。所以原子性指的就是,一个操作或者多个操作不会被别的因素打断,要不就一起执行,要不就一起都不执行。
- 比如在mysql中事务的操作也具有原子性。
- 常见的原子性操作有
-
- 直接对变量进行赋值
-
- 所有引用reference的赋值操作
-
- java.concurrent.Atomic包里面的操作
可见性
- 可见性指的是在多线程的情况下,如果有一个线程进行了变量的修改,那其他的线程都能看到。
- java中一般用volatile关键字来保证可见性
- 其原理是当一个变量被volatile关键字修饰了以后,线程本地的内存就无效了,会被存放到主内存中。
有序性
- 有序性指的是java的程序运行是不是按照代码的顺序来运行的。
- 在单线程的时候,一定是有序的。(线程内表现为串行语义)
- 但如果是多线程的情况下,不一定会有序。(重排序)
- volatile可以保证一定的有序性(比如单例模式中的dcl双重检查锁)
- synchronized和lock也可以保证,因为同一时间只有一个线程在工作
锁具有可见性和互斥性
- 互斥性指的是一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据
- 可见性就是上面描述的
volatile的特性
- 写一个volatile变量的时候,JMM会强制把这个值从线程本地内存写到主内存中(保证可见性)
- 禁止指令重排序
-
- 因为jvm内部如果一条程序的先后顺序不影响其逻辑,那么有可能会进行指令重排序
-
- 比如a = 2; b =3 ; c= a+b;我们写上去的是这样的,但是在虚拟机内部可能会进行重排序 比如先给b赋值3再给a赋值2,但是a+b一定是最好处理的,因为要保证最后输出的结果是c=a+b=5;
-
- 再举一个代码的例子(伪)
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
instance(memory); //2.初始化对象
-
- 上面的就是相当于new一个新对象的时候内存的变动,在单线程的时候这样变动是无所谓的,但是如果是多线程的话,可能会导致instance指向一个已经被分配的内存地址。
应用volatile特性写的一个DCL(双重检查锁)单例模式
public class DCL {
private volatile static DCL dcl;
private DCL(){}
public static DCL getInstance() {
if(dcl == null){ //此处是第一次检查
Syncronized(DCL.class){
if(dcl == null) {//此处是第二次检查
dcl = new DCL;
}
}
}
return dcl;
}
}
为什么要这么写 就是因为上面伪代码中,初始化对象和为新对象分配内存地址的时候发生了重排序,位置变动了,就导致第二个线程认为dcl已经不是空了(因为他已经有了指向的内存地址,尽管还没有被初始化),从而直接return了一个空的没有被初始化的dcl实例对象。
volatile的原理
- 通过内存屏障来实现的,如果反编译class文件你会发现,会多一行lock指令,这就是内存屏障
- 内存屏障提供的功能
-
- 内存屏障前后的东西不会因为指令重排序而导致位置互换。也就是所谓的当cpu执行到内存屏障的内容时,前面的一定全部完成。
-
- 强制将对缓存的修改(本地内存)写入到主内存中。
-
- 如果是写操作,会强制其他的cpu核心中的缓存行无效。