volatile 的主要作用有两个方面【可见性/顺序性[防止指令重排序]】
可见性:
首先熟悉一下JVM的内存工作模型[注意这里的工作模型不是堆/栈/方法区这些],
线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。因此要实现volatile变量的可见性,直接从这方面入手即可。对volatile变量的写操作与普通变量的主要区别有两点:1 修改volatile变量时会强制将修改后的值刷新的主内存中。 2 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。通过这两点就可以很好的解决可见性问题。
顺序性[防止指令重排序]:
volatile之所以能够阻止指令重排,是因为底层JVM里面利用了内存屏障来实现的,内存屏障主要有三点功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
举个栗子:
public class Singleton { public static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (instance) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
双重校验的写法:第一次判断是否为null是为了拒绝掉当对象不为空的时候剩余的线程。里面加锁是为了当对象为null的时候,此时同时进来两个线程(A和B两个线程),我们要保证只有一个线程才可以初始化对象,所以在这里面加上了锁,这样A拿到了锁进去初始化对象,然后进行返回,B再进去此时发现不为null,那么就不执行初始化的过程。这样就能保证上面的单例模式的正常运行,同时为系统也是节约了许多开销(避免每个线程进来加锁--懒汉式写法)
并发编程中谈及到的无非是可见性、有序性及原子性,为什么volatile不能保障原子性?
因为 volatile 只能修饰变量,但是有些变量的操作就没办法保障了,比如 i++ ,看JVM变异的指令其实是多条语句的,所以不能保障原子性,只能通过加锁的形式帮助去解决。