volatile关键字
Java内存模型
在JDK1.2
之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进⾏特别的注意的。⽽在当前的 Java 内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。
要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使⽤它都到主存中进⾏读取。
保证共享变量可见性的两种方式:
(1)加锁
- 线程获得锁
- 清空当前工作内存
- 从主内存中拷贝共享变量最新值到工作内存成为副本
- 执行代码,如果更新了共享变量的值,刷新回主内存中
- 释放锁
(2)使用volatile
修饰共享变量
- 线程a从主内存读取共享变量到对应的工作内存
- 对共享变量进行更改
- 线程b读取共享变量的值到对应的工作内存
- 线程a将修改后的值刷新到主内存,失效其他线程对共享变量的副本
- 线程b对共享变量进行操作时,发现已经失效,重新从主内存读取最新值,放入到对应工作内存
volatile
的特性
(1)volatile
不能保证原子性
(2)volatile
保证数据的可见性
(2)使用volatile
禁止指令重排序
保障volatile
原子性操作的方式
(1)加锁机制
(2)使用原子类
比如AtomicInteger
,在底层它是使用了volatile
来修饰变量,保证了数据的可见性,同时基于CAS机制
,保证了数据的原子性。
volatile
在单例模式中的应用
懒汉式(volatile
双重检查模式)
点击进入_8种单例模式的实现方式
public class Singleton6 {
//2.提供静态变量保存实例对象
private volatile static Singleton6 INSTANCE;
//1.私有化构造器
private Singleton6(){}
//3.提供获取对象的方法
public static Singleton6 getInstance(){
//第一重检查:针对很多个线程同时想要创建对象的情况
if(INSTANCE == null){//线程C
//同步代码块锁定
synchronized (Singleton6.class){
//第二重锁检查(针对比如A,B两个线程都为null,第一个线程创建完对象,第二个等待锁的线程拿到锁的情况)
if(INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
问题1:原子操作
再经过第二重锁检查
后,开始创建对象,但实际上创建对象的过程分几个步骤:
- 分配内存空间
- 调用构造器,初始化实例
- 返回地址引用
此时new Singleton6()
可能是一个非原子操作,编译器可能会重排序【只是在内存中开辟一片储存区域就直接返回给引用,还未进行赋值操作】,但此时线程C
想要创建对象,即经过第一重检查
,但此时对象只是一个半成品【不为null
,但未进行初始化】,当此线程C
拿着这个对象进行数据操作时,就会发生空指针异常
。
问题2:可见性
假如前面都执行正常,也没有发生重排序操作,此时线程A
创建了实例对象,但是还未通刷新到主内存中,此时线程B
想要创建实例对象,即过第一重检查
【但此时主内存中的INSTANCE
仍为null
】,那么线程B
又将再自己的工作内存中创建一个实例对象,这样就创建了多个实例对象。
针对上面两个问题,对静态实例变量使用volatile
修饰,它的两个作用,第一个禁止重排序
【创建对象时不会打乱顺序】,第二个保证可见性
【每次一个线程对共享变量进行了修改后,刷新更新后的值到主内存中,同时使其他线程工作内存中的变量副本失效,这样当线程C
再经过第一重检查时,会从主内存中重新取得共享变量的值,发现INSTANCE
不为null
,不进入代码】
volatile
的使用场景
(1)纯赋值操作,不适合做a++
操作
如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以使用volatile
代替synchronized
或者代替原子变量,因为赋值操作本身是原子性的,而volatile
又保证了可见性,所以可以保证线程安全。
(2)触发器
volatile
作为刷新之前变量的触发器。我们可以将某个变量使用volatile
修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见的。【基于volatile
的可见性和禁止重排序以及happen-before规则
实现】
volatile与synchronized的区别
volatile
只能修饰实例变量和类变量,而可以synchronized
修饰实例方法和代码块。
volatile
保证数据的可见性,但是不能保证数据的原子性,而synchronized
两者都能保证【是一种互斥机制】。
volatile
可以禁止指令重排序,可以检查使用单例双重检查中对象创建时执行代码的乱序问题。
volatile
可以看做是轻量版的synchronized
,volatile
不保证原子性,但是如果对一个共享变量进行多线程的纯赋值操作
,那么就可以使用volatile
代替synchronized
,因为赋值本身是原子性的,而volatile
保证了可见性,即可以保证是线程安全的。
小结:volatile
的使用
(1)volatile
适用以下场景:某个属性被多个线程共享,其中一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag
;作为触发器,实现轻量级同步。
(2)volatile
属性的读写都是无锁的,它不能代替synchronized
,因为它没有提供原子性和互斥行,正因为无锁,不需要花费时间在获取锁和释放锁上,低成本。
(3)volatile
只能修饰属性,这样编译器就不会对这个属性做指令重排序。
(4)volatile
保证了可见性,任何一个线程对共享变量的修改对其他线程可见,即volatile
修饰的属性不会被缓存,始终从主存中读取。
(5)volatile
提供了happen-before规则
保证对volatile
变量v的写入happen-before
所有其他线程对v的读操作。
(6)volatile
使得long
和double
的赋值是原子的。
(7)volatile
在单例双重检查中实现可见性
和禁止指令
重排序,从而保证线程安全性。
获取参考资料 提取码:jizh