单例模式及其线程安全问题
文章目录
- 单例模式及其线程安全问题
- 单例模式
- 定义
- 单例模式的写法(饿汉式、懒汉式)
- 饿汉式与懒汉式的应用场景区别
- 懒汉式单例模式的线程安全问题分析
- 线程安全问题
- 解决方案
单例模式
定义
在当前进程中,通过单例模式创建的类有且只有一个实例。
单例模式有如下几个特点:
- 在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在
- 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
- 没有公开的set方法,外部类无法调用set方法创建该实例
- 提供一个公开的get方法获取唯一的这个实例
单例模式的好处:
- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
- 省去了new操作符,降低了系统内存的使用频率,减轻GC压力
- 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
- 避免了对资源的重复占用
单例模式的写法(饿汉式、懒汉式)
饿汉式
public class Singleton {
// 创建一个实例对象
private static Singleton instance = new Singleton();
/**
* 私有构造方法,防止被实例化
*/
private Singleton(){}
/**
* 静态get方法
*/
public static Singleton getInstance(){
return instance;
}
}
饿汉式单例模式提前把对象new出来,这样别人哪怕是第一次获取这个类对象的时候直接就存在这个类了,省去了创建类这一步的开销。
懒汉式(线程不安全版本)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式单例模式在第一次被调用时初始化实例。
饿汉式与懒汉式的应用场景区别
在很多电商场景,如果这个数据是经常访问的热点数据,就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热),这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。
而懒汉式可以用在不怎么热的地方,比如那个数据你不确定很长一段时间是不是有人会调用,那就用懒汉式。
懒汉式单例模式的线程安全问题分析
线程安全问题
简单的说,就是线程一在创建实例但还未创建完毕的过程中线程二介入,此时线程二判断实例依然为空,故执行创建实例操作。
解决方案
- 加synchronized线程锁
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
缺点
- 只在创建实例过程中加锁(双检锁)
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized (Singleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
双检锁问题分析:
- 关键点:
instance = new Singleton()
不是原子操作。 - 详细分析:
- A、B线程同时进入了第一个if判断
- A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
- 由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
- B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
- 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
- 解决方案
- 加上volatile修饰Singleton,保证其可见性
- 通过volatile修饰的变量,不会被线程本地缓存,所有线程对该对象的读写都会第一时间同步到主内存,从而保证多个线程间该对象的准确性
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized (Singleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
volatile的作用
- 防止指令重排序,因为instance = new Singleton()不是原子操作
- 保证内存可见
使用volatile修饰后的问题:
- volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高
- 进一步优化方案:使用静态内部类
public class Singleton {
/* 私有构造方法,防止被实例化 */
private Singleton() {
}
/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 获取实例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
}
使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。
这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕, 这样我们就不用担心上面的问题。
同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式。