Volatile关键字、如何实现可见性、单例中怎么用?
4.volatile
volatile自身的特性
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。volatile修饰变量对可见性的影响所产生的价值远远高于变量本身,线程A写volatile变量后,线程B读该变量,那么所有A执行写操作执行可见的共享变量的值,在B读取volatile变量后也成为对B可见的了。
- 有序性:volatile会通过禁止指令重排序来保证有序性
- 原子性:对任意单个volatile变量的读/写具有原子性(哪怕它是64位的long或者double),但像volatile++这种复合操作不具有原子性
volatile的内存可见性实现原理
导致内存不可见的原因是 线程的本地缓存和内存之间的值不一致导致的,而volatile的读写实现了 缓存一致性(MESI协议) ,其底层原因是因为volatile变量进行写操作时会多一行Lock前缀的汇编代码,使得:
当前CPU缓存行的数据写回到系统内存
并通过总线让其他CPU里缓存了该内存地址的本地缓存无效(I)
追问:如何通过总线让其他缓存了该内存的CPU本地缓存无效(I)?
答:每个CPU会通过嗅探总线上的数据来查看本地缓存的数据是否过期,一旦CPU发现本地缓存对应的内存被修改,就会将本地缓存设为 无效(I)状态,此后CPU要再想获取这个数据就必须重新填充本地缓存,彼时会将缓存行标记为 共享(S)状态。
volatile的有序性实现原理
导致有序性问题的原因是 指令重排序,而volatile变量会使编译器再生成字节码时插入内存屏障来禁止指令重排序。
- 内存屏障的作用是保证特定操作的执行顺序:
- 对于Volatile变量进行写操作时,会在写操作后加上一个store屏障指令,将本地缓存中的共享变量值立刻刷新到内存中,并且不会将store屏障之前的代码排在store屏障之后
- 对于Volatile变量进行读操作时,会在读操作前面加上一个load屏障指令,马上读取主内存中的数据,并且不会将load屏障之后的代码排在load屏障之前
单例模式中如何使用Volatile?
追问:工作中哪里用到Volatile了?
答:在多线程下保证单例模式,volatile关键字必不可少,否则即使使用DCL双检锁也会由于指令重排序导致有序性问题,可能引发空指针异常。
手写一个volatile的单例模式
volatile+DCL双检锁可以实现线程安全的单例模式,但是不代表单例是安全的。
追问:那Spring容器的bean是线程安全的吗?
答:Spring容器本身并没有为bean提供线程安全的策略。
- 默认情况下,bean的Scope是单例的,
- 如果单例bean是一个无状态的bean,线程只能对它做查询操作,那这个bean是安全的,例如SpringMVC中的Controller、Service和Dao;
- 如果是有状态的bean,那在并发环境下就会导致竞态条件(原子性问题)和数据竞争(可见性问题),就不是线程安全的。
- 原型bean不会产生竞争,所以是线程安全的。
public class VolatileSingleton { /** * 私有化构造方法、只会构造一次 */ private VolatileSingleton(){ System.out.println("构造方法"); } private static volatile VolatileSingleton instance = null; public static VolatileSingleton getInstance(){ if(instance == null){ synchronized (VolatileSingleton.class){ if(instance == null){ instance = new VolatileSingleton(); } } } return instance; } public static void main(String[] args) { // new 30个线程,观察构造方法一共被调用几次 for (int i = 0; i < 30; i++) { new Thread(()->{ VolatileSingleton.getInstance(); }).start(); } // 输出:构造方法 } }#Java开发##内推##春招##实习##笔试题目##面经##笔经##Java#