面试题:volatile
volatile
1.volatile保证可见性
代码比较简单,我就不贴出来了。
-
子线程t从主内存读取到数据放入其对应的工作内存
-
将flag的值更改为true,但是这个时候flag的值还没有写会主内存
-
此时main方法main方法读取到了flag的值为false
-
当子线程t将flag的值写回去后,失效其他线程对此变量副本
-
再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中
总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。
保证可见性的原理
-
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
-
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
2.禁止指令重排序
问题代码示例:
/**
* @author WGR
* @create 2020/12/30 -- 21:10
*/
public class OutOfOrderDemo06 {
// 新建几个静态变量
public static int a = 0 , b = 0;
public static int i = 0 , j = 0;
public static void main(String[] args) throws Exception {
int count = 0;
while(true){
count++;
a = 0 ;
b = 0 ;
i = 0 ;
j = 0 ;
// 定义两个线程。
// 线程A
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
i = b;
}
});
// 线程B
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
j = a;
}
});
t1.start();
t2.start();
t1.join(); // 让t1线程优先执行完毕
t2.join(); // 让t2线程优先执行完毕
// 得到线程执行完毕以后 变量的结果。
System.out.println("第"+count+"次输出结果:i = " +i +" , j = "+j);
if(i == 0 && j == 0){
break;
}
}
}
}
发生了重排序:在线程1和线程2内部的两行代码的实际执行顺序和代码在Java文件中的顺序是不一致的,代码指令并不是严格按照代码顺序执行的,他们的顺序改变了,这样就是发生了重排序,这里颠倒的 是 a = 1 ,i = b 以及j=a , b=1 的顺序,从而发生了指令重排序。直接获取了i = b(0) , j = a(0)的值!显然这个值是不对的。
但是加上volatile关键字就会解决问题。
按照happens-before规则,我们只需要给b加上volatile,那么b之前的写入( a = 3;)将对读取b之后的代码可见,也就是说即使a不加volatile,只要b读取到3,那么b之前的操作(a=3)就一定是可见的,此时就绝对不会出现b=3的时候而读取到a=1了。
happens-before规则可以看我这个面试题:https://www.cnblogs.com/dalianpai/p/14212690.html
-
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
-
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
读写屏障可以参考这个面试题:https://www.cnblogs.com/dalianpai/p/14162021.html
3. volatile在双重检查加锁的单例中的应用
单例概述
- 单例是需要在内存中永远只能创建一个类的实例,
- 单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。
单例模式的适用场景:
- 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
- 无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。
单例模式有8种
单例模式我们可以提供出8种写法,有很多时候我们存在饿汉式单例的概念,以及懒汉式单例的概念。
- 饿汉式单例的含义是:在获取单例对象之前对象已经创建完成了。
- 懒汉式单例是指:在真正需要单例的时候才创建出该对象。
饿汉单例的2种写法
特点:在获取单例对象之前对象已经创建完成了。
饿汉式(静态常量)
/**
目标:饿汉式(静态常量)
步骤:
1.构造器私有。
2.定义一个静态常量保存一个唯一的实例对象(单例)
3.
*/
public class Singleton01 {
// 2.定义一个静态常量保存一个唯一的实例对象(单例)
private static final Singleton01 INSTANCE = new Singleton01();
// 1.构造器私有。
private Singleton01(){
}
// 3.提供一个方法返回单例对象。
public static Singleton01 getInstance(){
return INSTANCE;
}
}
class Test01{
public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
System.out.println(s1 == s2);
}
}
饿汉式(静态代码块)
/**
目标:饿汉式(静态代码块)
步骤:
1.构造器私有。
2.定义一个静态常量保存一个唯一的实例对象(单例),可以通过静态代码块初始化单例对象。
3.提供一个方法返回单例对象。
*/
public class Singleton02 {
// 2.定义一个静态常量保存一个唯一的实例对象(单例)
private static final Singleton02 INSTANCE ;
static{
INSTANCE = new Singleton02();
}
// 1.构造器私有。
private Singleton02(){
}
// 3.提供一个方法返回单例对象。
public static Singleton02 getInstance(){
return INSTANCE;
}
}
class Test02{
public static void main(String[] args) {
Singleton02 s1 = Singleton02.getInstance();
Singleton02 s2 = Singleton02.getInstance();
System.out.println(s1 == s2);
}
}
懒汉式单例4种写法
特点:在真正需要单例的时候才创建出该对象。在Java程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在使用这些对象的时候才初始化,此时,程序员可能会采用延迟初始化。值得注意的是:要正确的实现线程安全的延迟初始化还是需要一些技巧的,否则很容易出现问题。
懒汉式(线程不安全)
/**
目标:懒汉式(线程不安全的写法)。
步骤:
1.构造器私有。
2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
没有就创建一个新的单例对象。
*/
public class Singleton03 {
// 2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
private static Singleton03 INSTANCE;
// 1.构造器私有。
private Singleton03(){
}
// 3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
// 没有就创建一个新的单例对象。
public static Singleton03 getInstance(){
if(INSTANCE == null){
// 说明这是第一次来拿单例对象,需要真正的创建出来!
INSTANCE = new Singleton03();
}
return INSTANCE;
}
}
懒汉式(线程安全,性能差)
使用synchronized关键字修饰方法包装线程安全,但性能差多,并发下只能有一个线程正在进入获取单例对象。
/**
目标:懒汉式(线程安全的写法)。
步骤:
1.构造器私有。
2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
没有就创建一个新的单例对象。
4.为获取单例的方法加锁:用synchronized
*/
public class Singleton04 {
// 2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
private static Singleton04 INSTANCE;
// 1.构造器私有。
private Singleton04(){
}
// 3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
// 没有就创建一个新的单例对象。
// 懒汉式线程安全的写法:线程A , 线程B.
public synchronized static Singleton04 getInstance(){
if(INSTANCE == null){
// 说明这是第一次来拿单例对象,需要真正的创建出来!
INSTANCE = new Singleton04();
}
return INSTANCE;
}
}
懒汉式(线程不安全)
特点:是一种优化后的似乎线程安全的机制。
/**
目标:懒汉式(线程不安全)
步骤:
1.构造器私有。
2.定义一个静态变量存储一个单例对象。
3.提供一个方法返回一个单例对象。
*/
public class Singleton05 {
// 2.定义一个静态变量存储一个单例对象。
private static Singleton05 INSTANCE ;
// 1.构造器私有
private Singleton05(){
}
// 3.返回一个单例对象
public static Singleton05 getInstance(){
// 判断单例对象的变量是否为null
if(INSTANCE == null){
// 很多个线程执行到这里来:A , B
synchronized (Singleton05.class){
INSTANCE = new Singleton05();
}
}
return INSTANCE;
}
}
懒汉式(volatile双重检查模式,推荐)
/**
目标:双重检查机制,以及使用volatile修饰(最好,最安全的方式,推荐写法)
步骤:
1.构造器私有。
2.提供了一个静态变量用于存储一个单例对象。
3.提供一个方法进行双重检查机制返回单例对象。
4.必须使用volatile修饰静态的变量。?
双重检查的优点:线程安全,延迟加载,效率较高!!
*/
public class Singleton06 {
// 2.提供了一个静态变量用于存储一个单例对象。
private volatile static Singleton06 INSTANCE;
// 1.构造器私有。
private Singleton06(){
}
// 3.提供一个方法进行双重检查机制返回单例对象。
public static Singleton06 getInstance(){
// 第一次检查:判断单例对象的变量是否为null
if(INSTANCE == null ){
// A , B
synchronized (Singleton06.class){
// 第二次检查:判断单例对象的变量是否为null
if(INSTANCE == null){
INSTANCE = new Singleton06();
}
}
}
return INSTANCE;
}
}
静态内部类单例方式
引入:JVM在类初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案
/**
目标:基于类的初始化实现延迟加载和线程安全的单例设计。
步骤:
1.构造器私有。
2.提供一个静态内部类,里面提供一个常量存储一个单例对象。
3.提供一个方法返回静态内部类中的单例对象。
*/
public class Singleton07 {
// 1.构造器私有。
private Singleton07(){
}
// 2.提供一个静态内部类,里面提供一个常量存储一个单例对象。
private static class Inner{
private static final Singleton07 INSTANCE = new Singleton07();
}
// .提供一个方法返回静态内部类中的单例对象。
// 线程A , 线程B
public static Singleton07 getInstance(){
return Inner.INSTANCE;
}
}
- 静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象。
- 通过对比基于volatile的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更简洁。但是基于volatile的双重检查锁定方案有一个额外的优势:除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化。
枚举实现单例
/**
目标:枚举实现单例。
引入:枚举实际上是一种多例的模式。如果我们直接定义一个实例就相当于是单例了。
*/
public enum Singleton08 {
INSTANCE;
}
4.小结
应用场景
-
赋值操作,volatile不适合做a++等操作。 如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
-
触发器,按照volatile的可见性和禁止重排序以及happens-before规则,volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见。
volatile和synchronized区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
volatile的总结
- volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得修改后的值,比如boolean flag ;或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized ,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
- volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
- volatile提供了happens-before保证,对volatile变量v的写入happens- before所有其他线程后续对v的读操作。
- volatile可以使得long和double的赋值是原子的。
- volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。