单例模式及其线程安全问题


文章目录

  • 单例模式及其线程安全问题
  • 单例模式
  • 定义
  • 单例模式的写法(饿汉式、懒汉式)
  • 饿汉式与懒汉式的应用场景区别
  • 懒汉式单例模式的线程安全问题分析
  • 线程安全问题
  • 解决方案


单例模式

定义

在当前进程中,通过单例模式创建的类有且只有一个实例

单例模式有如下几个特点:

  • 在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;  
    }  
}

懒汉式单例模式在第一次被调用时初始化实例。


饿汉式与懒汉式的应用场景区别

在很多电商场景,如果这个数据是经常访问的热点数据,就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热),这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。

而懒汉式可以用在不怎么热的地方,比如那个数据你不确定很长一段时间是不是有人会调用,那就用懒汉式。

懒汉式单例模式的线程安全问题分析

线程安全问题

单例模式java实现线程安全 java单例是线程安全的吗_设计模式

简单的说,就是线程一在创建实例但还未创建完毕的过程中线程二介入,此时线程二判断实例依然为空,故执行创建实例操作。

解决方案

  1. 加synchronized线程锁
public class Singleton {
    private static Singleton instance = null;

    private Singleton(){}

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

缺点

  1. 只在创建实例过程中加锁(双检锁)
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块。
  • 单例模式java实现线程安全 java单例是线程安全的吗_单例模式java实现线程安全_02

  • 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的内存初始化完毕, 这样我们就不用担心上面的问题。

同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式。