单例模式&&volatile

引用文章
23 种设计模式详解(全23种)
一个volatile跟面试官扯了半个小时

单例模式

定义:确保一个类最多只有一个实例,并提供一个全局访问点

单例模式可以分为两种:预加载和懒加载

预加载

顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。

public class PreloadSingleton {
       
       public static PreloadSingleton instance = new PreloadSingleton();
   
       //其他的类无法实例化单例类的对象
       private PreloadSingleton() {
       };
       
       public static PreloadSingleton getInstance() {
              return instance;
       }
}

很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。

懒加载

为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建。

public class Singleton {
       
       private static Singleton instance=null;
       
       private Singleton(){
       };
       
       public static Singleton getInstance()
       {
              if(instance==null)
              {
                     instance=new Singleton();
              }
              return instance;
              
       }
}

单例模式和线程安全

(1)预加载只有一条语句return instance,这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费。

(2)懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。

不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:

memory=allocate();//1:初始化内存空间
 
ctorInstance(memory);//2:初始化对象
 
instance=memory();//3:设置instance指向刚分配的内存地址

jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。我们用两个线程来说明线程是不安全的。线程A和线程B都创建对象。其中,A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象(线程不安全)。
在这里插入图片描述

保证懒加载的线程安全

我们首先想到的就是使用synchronized关键字。synchronized加载getInstace()函数上确实保证了线程的安全。**但是,如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。**为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。

public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     instance = new Singleton();
              }
              return instance;
       }
}

我们把sychronized加在if(instance==null)判断语句里面,保证instance未实例化的时候才加锁

public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (Singleton.class) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

我们经过2.3的讨论知道new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。

public class Singleton {
       private static volatile Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (instance) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

到此,我们就保证了懒加载的线程安全。

———————————————— 版权声明:本文为CSDN博主「鬼灭之刃」的原创文章,遵循CC 4.0
BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/A1342772/article/details/91349142

volatile

  • 安琪拉: volatile 除了让共享变量具有可见性,还具有有序性(禁止指令重排序)。
  • 面试官: 你先跟我举几个实际volatile 实际项目中的例子?
    安琪拉: 可以的。有个特别常见的例子:
    状态标志
    比如我们工程中经常用一个变量标识程序是否启动、初始化完成、是否停止等,如下:
    在这里插入图片描述

volatile 很适合只有一个线程修改,其他线程读取的情况。volatile 变量被修改之后,对其他线程立即可见。

  • 面试官: 现在我们来看一下你的例子,如果不加volatile 修饰,会有什么后果?
  • 安琪拉: 比如这是一个带前端交互的系统,有A、 B二个线程,用户点了停止应用按钮,A 线程调用shutdown() 方法,让变量shutdown 从false 变成 true,但是因为没有使用volatile 修饰, B 线程可能感知不到shutdown 的变化,而继续执行 doWork 内的循环,这样违背了程序的意愿:当shutdown 变量为true 时,代表应用该停下了,doWork函数应该跳出循环,不再执行。
  • 面试官: volatile还有别的应用场景吗?

懒汉式单例模式

  • 安琪拉: 懒汉式单例模式,我们常用的 double-check 的单例模式,如下所示:
    在这里插入图片描述
    使用volatile 修饰保证 singleton 的实例化能够对所有线程立即可见。
  • 面试官: 我们再来看你的单例模式的例子,我有三个问题:
  1. 为什么使用volatile 修饰了singleton 引用还用synchronized 锁?
  2. 第一次检查singleton 为空后为什么内部还需要进行第二次检查?
  3. volatile 除了内存可见性,还有别的作用吗?
  • 安琪拉: 【心里炸了,举单例模式例子简直给自己挖坑】这三个问题,我来一个个回答:
  1. 为什么使用volatile 修饰了singleton 引用还用synchronized 锁?
    volatile 只保证了共享变量 singleton 的可见性,但是 singleton = new Singleton(); 这个操作不是原子的,可以分为三步:
    步骤1:在堆内存申请一块内存空间;
    步骤2:初始化申请好的内存空间;
    步骤3:将内存空间的地址赋值给 singleton;

所以singleton = new Singleton(); 是一个由三步操作组成的复合操作,多线程环境下A 线程执行了第一步、第二步之后发生线程切换,B 线程开始执行第一步、第二步、第三步(因为A 线程singleton 是还没有赋值的),所以为了保障这三个步骤不可中断,可以使用synchronized 在这段代码块上加锁。(synchronized 原理参考《安琪拉与面试官二三事》系列第二篇文章)

  1. 第一次检查singleton 为空后为什么内部还进行第二次检查?
    A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次。

  2. volatile 除了内存可见性,还有别的作用吗?
    volatile 修饰的变量除了可见性,还能防止指令重排序。
    指令重排序 是编译器和处理器为了优化程序执行的性能而对指令序列进行重排的一种手段。现象就是CPU 执行指令的顺序可能和程序代码的顺序不一致,例如 a = 1; b = 2; 可能 CPU 先执行b=2; 后执行a=1;
    singleton = new Singleton(); 由三步操作组合而成,如果不使用volatile 修饰,可能发生指令重排序。步骤3 在步骤2 之前执行,singleton 引用的是还没有被初始化的内存空间,别的线程调用单例的方法就会引发未被初始化的错误。

指令重排序也遵循一定的规则:

  1. 重排序不会对存在依赖关系的操作进行重排
  2. 重排序目的是优化性能,不管怎样重排,单线程下的程序执行结果不会变

volatile的底层原理

  • Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,保证共享变量操作的有序性。

  • 内存屏障指令:写操作的会让线程本地的共享内存变量写完强制刷新到主存读操作让本地线程变量无效,强制从主内存读取,保证了共享内存变量的可见性。

  • JVM中提供了四类内存屏障指令:

在这里插入图片描述

并发&并行

现代操作系统,现代操作系统都是按时间片调度执行的,最小的调度执行单元是线程,多任务和并行处理能力是衡量一台计算机处理器的非常重要的指标。这里有个概念要说一下:

并发:多个程序可能同时运行的现象,例如刷微博和听歌同时进行,可能你电脑只有一颗CPU,但是通过时间片轮转的方式让你感觉在同时进行。

并行:多核CPU,每个CPU 内运行自己的线程,是真正的同时进行的,叫并行。

volatile的实现原理总结

volatile可以实现内存的可见性和防止指令重排序。

通过内存屏障技术实现的。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障指令,内存屏障效果有:

  1. 禁止volatile 修饰变量指令的重排序

  2. 写入数据强制刷新到主存

  3. 读取数据强制从主存读取

volatile使用总结

volatile 是Java 提供的一种轻量级同步机制,可以保证共享变量的可见性和有序性(禁止指令重排),常用于

状态标志、双重检查的单例等场景。使用原则:

对变量的写操作不依赖于当前值。例如 i++ 这种就不适用。

该变量没有包含在具有其他变量的不变式中。

volatile的使用场景不是很多,使用时需要仔细考虑下是否适用volatile,注意满足上面的二个原则。

单个的共享变量的读/写(比如a=1)具有原子性,但是像num++或者a=b+1;这种复合操作,volatile无法保证其原子性

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值