volatile
volatile关键字在Java面试中几乎是必考题
单例模式的双重检查模式(DCL)一般会引申到synchronized关键字和volatile关键字
volatile关键字了解吗?它保证了什么特性?用什么方式禁止指令重排的吗?
1)volatile保证了什么特性
-
保证线程可见性(多线程环境下保证内存可见性)
- 线程修改后的共享变量值能够及时刷新,从工作内存中刷新回主内存;
- 其它线程能够及时的把共享变量的值从主内存中更新到自己的工作内存中;
- Java 内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其 JVM 模型大致如下图。
-
禁止指令重排序优化(多线程模式下禁止指令重排序优化)
-
哪条代码需要指令重排?
instance = new instance();
使用了volatile关键字之后,重排序被禁止,所有的写操作(write)都发生在读操作(read)之前。
-
用什么方式禁止指令重排的吗?
内存屏障:
读屏障(Load Barrier):在读指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据,保证读取的是最新数据。
写屏障(Store Barrier):在写指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,保证写入的数据立刻对其他线程可见。
- 在工作内存中,每次使用V(volatile变量)前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改。
- 在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改。
2)volatile导致哪条代码需要指令重排?
instance = new Singleton();
会被编译器编译成如下JVM指令:
- memory = allocate(); //分配对象的内存空间
- ctorInstance(memory); //初始化对象
- instance = memory; //设置instance指向刚分配的内存空间
但是这些指令排序并非一成不变,有可能会经过JVM和CPU的优化,指令重排为如下顺序:
- memory = allocate(); //分配对象的内存空间
- instance = memory; //设置instance指向刚分配的内存空间
- ctorInstance(memory); //初始化对象
当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
实例化对象实际会分为3个步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但有的编译器由于性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
使用了volatile关键字之后,重排序被禁止,所有的写操作(write)都发生在读操作(read)之前。