单例模式&线程安全问题

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

文章目录

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

单例模式

定义

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

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

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

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


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

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

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

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

线程安全问题

单例模式&线程安全问题_第1张图片

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

解决方案

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

    缺点 严重降低系统运行性能

  2. 只在创建实例过程中加锁(双检锁)

    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块。单例模式&线程安全问题_第2张图片

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

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

你可能感兴趣的:(设计模式,设计模式,线程安全)