美文网首页
设计模式之单例模式

设计模式之单例模式

作者: WekingZhang | 来源:发表于2018-12-06 14:09 被阅读0次

    单例设计模式全解析

    目录:
    1. 懒汉方式
      1.1 非线程安全的单例模式
      1.2 线程安全的单例模式
        1.2.1 性能较差的同步单例模式
        1.2.2 双重检查锁
    2. 饿汉方式
      2.1 类加载实例化
    3. 静态内部类方式
    4. 枚举方式
    5. 总结
    
    

    在学习设计模式时,单例设计模式应该是学习的第一个设计模式,单例设计模式也是“公认”最简单的设计模式,但真实并非如此,本文将介绍了多种实现单例模式的方法。目前有三种方式可以实现单例模式,分别是

    1. 懒汉方式
    2. 饿汉方式
    3. 枚举方式

    1.懒汉方式

    懒汉方式由懒加载的工作方式而得名。根据线程安全与否,可以分为非线程安全和线程安全两种实现方式。

    1.1 非线程安全的单例模式

    非线程安全的单例实现方式是学习单例模式时接触到的第一个单例程序。虽然该程序是非线程安全的,但是能够更好的理解单例模式的核心思想。具体的代码如下:

    代码1:

    public class Singleton {
        //表示Singleton的唯一实例
        private static Singleton singleton = null;  
        private Singleton(){}
        public static Singleton getInstance(){
            //如果singleton的实例为null,则新建实例,否则返回创建好的实例对象
            if( singleton == null ){
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    
    

    从非线程安全的单例模式中,可以清楚看到,单例模式包含了一个私有的构造方法和一个静态方法,这是实现单例模式的必要条件。在Signleton类中,没有同步操作,所以是线程不安全的。

    1.2 线程安全的单例模式

    为了实现线程安全的单例模式,一般通过synchronized关键字实现,本次主要探讨通过synchronized关键字实现。

    1.2.1 性能较差的单例模式

    为了保证代码1中Singleton类线程安全,可以为getInstance方法增加synchronized关键字修饰。具体代码如下:

    代码2:

    public class Singleton {
        private static Singleton singleton = null;
        private Singleton(){}
        public synchronized static Singleton getInstance(){
            //如果singleton的实例为null,则新建实例,否则返回创建好的实例对象
            if( singleton == null ){
                singleton =  new Singleton();
            }
            return singleton;
        }
    }
    

    在代码2中实现了线程安全的getInstance方法,这样保证了在任何时刻,只能有一个线程调用getInstance方法。但是这种却是低效的,因为当单例对象创建后,所有线程仍然无法同时调用getInstance方法,即使在这时线程安全问题已经不存在。

    既然为方法增加synchronized关键字会给程序性能带来损失,那么有没有一种方式可以避免呢?理想的情况应该是当singleton实例为null时,才进行同步操作,否则直接返回singleton实例,这样就大大降低了synchronize关键字带来的性能损失。

    1.2.2 双重检查锁

    为了解决代码2中的同步方法带来的性能损失,依照1.2.1节最后提出的解决思路,本节主要介绍了双重检查锁,双重检查锁实现只有当单例没有实例化时,进行同步,否则直接返回实例,不进行同步,从而降低了同步带来的同步损失,具体如下述代码所示:

    代码3:

    public class Singleton {
        private static volatile Singleton singleton = null;
        private Singleton(){}
        public static Singleton getInstance(){
        
            if(singleton == null ){                     //A
                synchronized (Singleton.class){         //B
                    if(singleton == null ){             //C
                        singleton=new Singleton();      //D
                    }
                }
            }
            return singleton;
        }
    }
    
    

    代码3中通过双重检查锁实现了线程安全的单例模式,其中A行首先对singleton实例进行是否为null的判断,为了防止竞争,通过synchronized代码块实现同步,程序是对Singleton Class 实现同步,从而实现在任何一个时刻只能有一个类实例访问同步块代码,在代码内部继续进行对singleton进行了是否为null判断,当两个条件同时满足,才新建实例,并且返回实例对象。同时应该注意类属性Singleton由volatile关键字修饰,这也是保证线程安全的关键部分,下面通过两个问题,进一步理解上述代码:

    • 为什么要进行两次NUll判断?请说明两次NULL判断存在的必要性。

      答:首先需要说明,两次非空判断是为了保证在多线程的环境下实现线程安全。行A NULL判断实现了如果singleton is not null 时,直接返回实例。

      为了进一步说明,现假设存在线程1和线程2,在某一个时刻(singleton未被实例化),线程1运行到了行C,线程2运行到了A行。此时,线程2判断singleton is null 从而进入if体内,由于没有Singleton.class的同步锁,只能等待下去;接着,线程1判断singleton同样为null,继续运行D行并且返回了新建的实例,注意此时singleton已经被实例化。线程1结束,由于线程1释放的同步锁,从而线程2获得了同步锁,继续运行同步块内的部分,假设行D的null判断不存在,此时将返回新的Singleton对象,这样就无法实现单例模式。所以两次null判断都是非常必要的。

    • 请解释一下volatile关键字的在程序中的作用

      答:在多线程编程环境中,经常会用到了volatile关键字,该关键字保证了每个线程在栈中不会保存该变量的副本,每次都是从主内存中读取该变量,从而保证变量对于每个线程的可见性。然而volatile还有一个重要的特性:禁止指令的重排序优化。也就是说volatile变量的赋值操作会有个内存屏障,读操作不会被重排序到写操作之前。

      介绍了Volatile关键字的作用,为了解释volatile在程序中的作用,首先解析一下行D,表面上行D只有一行代码,遗憾的是该行代码不是原子性的。事实上,JVM虚拟机会解析成三个操作:

      (1) 为singleton变量分配内存;

      (2) 调用Singleton构造函数,初始化成员变量,初始化Singleton的内存空间

      (3) singletion变量指向创建的Signleton的内存空间

      当步骤(3)执行完成后,singleton就不再是null。由于JVM内部存在指令优化,上述三个步骤的顺序可能被打乱,存在(1)(2)(3)和(1)(3)(2)的执行顺序。

      假设存在线程1和线程2,在没有volatile关键字修饰变量的情况下,线程1运行至行D,而线程2还没有开始执行。由上述分析可知行D被解析成三个原子操作,并且存在多种执行顺序,假设当前的执行顺序是(1)(3)(2)。如果当线程1执行完(3)并且在(2)执行之前,线程2开始执行可以看到singleton此时已经指向某块内存空间,不再是null,线程执行行A然后就返回singleton,当调用singleton就报错了。这是因为singleton所指向的内存空间其实还不是Singleton的内存空间,并没有进行初始化操作。

      当存在volatile关键字时,volatile保证了三个操作执行完毕后,才允许进行读操作(读操作不会被重排序到写操作之前),从而避免了上述可能出现的错误。

    2. 饿汉方式

    相比于懒汉方式的懒加载方式,饿汉方式就是另外一种非懒加载的方式。本节将详细介绍饿汉方式中的类加载实例化方式。废话不多说,请看下面的代码:

    代码4:

    public class Singleton {
        private static final Singleton singleton = new Singleton();
        private Singleton(){}
        public static Singleton getInstance(){
            return singleton;
        }
    }
    

    在代码4中,最明显的不同是静态方法getInstance()中的判断逻辑几乎没有了,代码显得更加的简洁。但是也应该注意到,singleton的实例化操作放在了属性声明的位置,如果对static关键字非常了解的话,就应该知道属性singleton的实例化操作是当Singleton类首次被加载(使用)时完成的,也就是说即使不调用getInstance方法,singleton的仍然被实例化,并一直放在了堆内存中。

    虽然类加载实例化的方式使得代码的判断逻辑简单了许多,但是该方式仍然有一个明显的缺陷就是:即使程序中不需要单例对象,只要单例类被加载到内存中,单例对象就一直在内存中存在,如果内存相对稀缺的话,那么将是灾难性的。

    3. 静态内部类方式

    为了解决饿汉方式所带类的问题,本节将详细地介绍静态内部类方式。具体的代码如下所示:

    代码5:

    public class Singleton {
        private  static class SingletonHolder{
            private static final Singleton singleton = new Singleton();
        }
        private Singleton(){}
    
        public static Singleton2 getInstance(){
            return SingletonHolder.singleton;
        }
    }
    
    

    代码5中使用了静态内部类的方式实现了调用时的实例化方式,即是只有当getInstance()方法被调用时,才实例化Singleton的实例。由于单例对象是当调用时才进行实例化,该方式其实是属于懒汉方式。

    4. 枚举方式

    枚举是为了描述有限个状态的数据结构,根据《JAVA编程思想》的介绍,枚举类与普通类基本相同,只是枚举类中的几个有限的值都是该枚举类的静态实例化对象。更多关于枚举类的相关信息,可以深入阅读《JAVA编程思想》的相关章节。下面介绍一种方式实现,具体请看下面的代码:

    代码6:

    public enum Singleton {
        INSTANCE;
    }
    
    

    代码6是最简单的枚举,可以通过Singleton.INSTANCE访问单例实例。如果想要为INSTANCE增加更多的“功能”,可以在枚举类Singleton增加相关的方法,通过Singleton.INSTANTCE.methodName()来调用。由于创建枚举是线程安全的,所以没有必要担心线程安全问题,并且还可以防止反序列化创建新的对象。这也是《Effective Java》中推荐使用的单例方式。

    5. 总结

    本文主要对单例模式进行全面的解析,以逐步递进的方式介绍各种实现单例模式的方式,依次介绍了非线程安全的懒汉方式、两种线程安全的懒汉方式(性能损失的单例模式和双重检查锁)、饿汉方式、静态内部方式、枚举方式。在实际工作中,可以选用静态内部类和枚举类方式来实现单例模式。

    相关文章

      网友评论

          本文标题:设计模式之单例模式

          本文链接:https://www.haomeiwen.com/subject/vydwsttx.html