单例模式-一问到底

Posted 双斜杠少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式-一问到底相关的知识,希望对你有一定的参考价值。

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式。在 GOF 书中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式有以下两个优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。

  • 避免对资源的多重占用(比如写文件操作)。

有时候,我们在选择使用单例模式的时候,不仅仅考虑到其带来的优点,还有可能是有些场景就必须要单例。比如类似"一个党只能有一个主席"的情况。

实现

  • 懒汉,
  • 饿汉,
  • 双重校验锁,
  • 枚举
  • cas

1. 懒汉模式

所谓"懒汉",指的是并不会事先初始化出单例对象,而是在第一次使用的时候再进行初始化,懒汉模式有两种写法,分别是线程安全的和非线程安全的。

懒汉模式可以在第一次真正用到的时候再实例化,避免了创建无效的对象。但是缺点是第一次使用的时候需要耗费时间进行对象的初始化。

//线程不安全的懒汉模式:
public class Singleton 
    private static Singleton instance;
    private Singleton ()

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


//线程安全的懒汉模式:
public class Singleton 
    private static Singleton instance;
    private Singleton ()
    public static synchronized Singleton getInstance() 
    if (instance == null) 
        instance = new Singleton();
    
    return instance;
    

2. 饿汉模式

就是使用static定义全局对象,在对象声明的时候直接new一个对象,或者使用静态代码块。

所谓"饿汉",是指等不及要赶紧创建单例对象,即在类加载的过程中就进行单例对象的创建。具体实现方式也有多种。

//静态成员变量 饿汉模式:
public class Singleton   
    private static Singleton instance = new Singleton();
    private Singleton ()
    public static Singleton getInstance() 
    return instance;
    


//静态代码块 饿汉模式变种:
public class Singleton 
    private Singleton instance = null;
    static 
    instance = new Singleton();
    
    private Singleton ()
    public static Singleton getInstance() 
    return this.instance;
    


//静态内部类
public class Singleton   
    private static class SingletonHolder   
    private static final Singleton INSTANCE = new Singleton();  
      
    private Singleton ()  
    public static final Singleton getInstance()   
    return SingletonHolder.INSTANCE;  
      
  

2.1 饿汉如何保证线程安全

通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。和通过定义静态的内部类,以保证单例对象可以在类初始化的过程中被实例化。

这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

静态内部类,这种方式和静态代码块、成员变量只有细微差别,只是做法上稍微优雅一点。这种方式是Singleton类被装载了,INSTANCE 不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。。。但是,原理和饿汉一样。

所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。

3. 双重校验锁单例

通过同步代码块代替了懒汉模式中的同步方法,来减小锁的粒度,减少阻塞。但是避免并发,需要进行两次非空判断,所以叫做双重锁校验。

3.1双重锁校验的单例中为什么要使用volatile

因为编译器有可能进行指令重排优化,使得singleton对象再未完成初始化之前就对其进行了赋值,这样其他人拿到的对象就可能是个残缺的对象了。使用volatile的目的是避免指令重排。保证先进性初始化,然后进行赋值

//双重校验锁实现单例:
public class Singleton 
    private volatile static Singleton singleton;
    private Singleton ()
    public static Singleton getSingleton() 
    if (singleton == null) 
        synchronized (Singleton.class) 
            if (singleton == null) 
                singleton = new Singleton();
            
        
    
    return singleton;
    

3.2 指令重排

java内存模型(jmm)并不限制处理器重排序,在执行**instance=new Singleton();**时,并不是原子语句,实际是包括了下面三大步骤:

  • 1.为对象分配内存
  • 2.初始化实例对象
  • 3.把引用instance指向分配的内存空间

这个三个步骤并不能保证按序执行,处理器会进行指令重排序优化,存在这样的情况:
优化重排后执行顺序为:1,3,2, 这样在线程1执行到3时,instance已经不为null了,线程2此时判断instance!=null,则直接返回instance引用,但现在实例对象还没有初始化完毕,此时线程2使用instance可能会造成程序崩溃。
现在要解决的问题就是怎样限制处理器进行指令优化重排。

5.volatile double check 懒汉模式
在JDK1.5之后,使用volatile关键字修饰instance就可以实现正确的double check单例模式了

4. 枚举实现单例

public enum  DataSourceEnum 
    DATASOURCE;
    private DBConnection connection = null;
    private DataSourceEnum()
        connection = new DBConnection();
    
    public DBConnection getConnection()
        return connection;
    
 

枚举实现单例的好处

除了写法简单,几行代码就能搞定了,线程安全(枚举其实底层是依赖Enum类实现的,这个类的成员变量都是static类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的。)以外,枚举还有一个好处,那就是"枚举可以解决反序列化会破坏单例的问题"

在枚举序列化的时候,Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。

5. CAS(AtomicReference)实现单例

不使用synchronized和lock,怎么实现一个线程安全的单例?
借助CAS(AtomicReference)实现单例模式:

public class Singleton 
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 

    private Singleton() 

    public static Singleton getInstance() 
        for (;;) 
            Singleton singleton = INSTANCE.get();
            if (null != singleton) 
                return singleton;
            

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) 
                return singleton;
            
        
    

用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。 CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

参考:
单例模式的七种写法:https://www.iteye.com/blog/cantellow-838473
他轻蔑的问我:你还说你了解单例模式吗:https://mp.weixin.qq.com/s/L12lHC0-ieEGqTDmIDkNtQ

以上是关于单例模式-一问到底的主要内容,如果未能解决你的问题,请参考以下文章

Java -- 每日一问:谈谈你知道的设计模式?

单例模式

面试官猛的一问:Spring的Bean注入如何解决循环依赖的?

面试官猛的一问:Spring的Bean注入如何解决循环依赖的?

蓦然回头-单例模式篇章一

单例模式讨论篇:单例模式与垃圾回收