前提知识:并发编程三个特性——原子性、可见性、一致性
参考并发编程(原子性、原子性、可见性、一致性)
原子性:锁,synchronized保证
可见性:volatile保证
有序性:多方面共同保障,如下
1、synchronized
2、lock
3、volatile(一定程度的有序性)
synchronized和lock在同一时刻只能让一个线程执行同步方法,当然就是有序的了。
volatile的有序性是指写入修改后的volatile变量这个操作必定优先于 读取这个变量。
volatile的作用
(1)保证并发编程的可见性(不保证原子性)
- 单线程下修改读取数据时Java内存模型没问题;但是多线程下可能读取到脏数据,需要用volatile修饰
- 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新的值
- volatile可见性是通过汇编加上Lock前缀指令,触发底层的MESI缓存一致性协议来实现的
(2)禁止指令重排序
- 指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序。单线程时重排序无影响,但是多线程时顺序错了,可能就不符合需求了,需要保证执行顺序的变量可以用volatile修饰。
- 典型例子:单例模式的懒汉式双重检测加锁实现方式
//懒汉式
public class Singleton{
//私有化构造函数
private Singleton(){}
//先不创建对象,等用到的时候再创建
private static volatile Singleton ins=null; //避免重排序,volatile有内存屏障的功能
//调用到这个方法了,证明是要被用到的了
public static Singleton getInstance(){
if(ins==null){ //这个判断主要为了提高性能
synchronized(Singleton.class){ //使用synchronized将锁的范围缩小,提高性能
if(ins==null){ //再判断一次是否为null(主要的判断)
ins=new Singleton();
}
}
}
return ins;
}
}
ins变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“ins= new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:
1)为ins分配内存
2)初始化ins
3)将ins变量指向分配的内存空间
由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现ins变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证ins变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。