设计模式:23种设计模式之单例模式

设计模式:23种设计模式之单例模式


前言:单例(Singleton)模式应该是开发者们最熟悉的设计模式了,并且好像也是最容易实现的,基本上每个开发者都能够随手写出。但是,真的是这样吗?往下看,我们一起来聊聊看!

一、单例(Singleton)模式的认识
    维基百科上对单例的定义为:单例对象的类必须保证只有一个实例存在。对单例的实现可以分为两大类:懒汉式饿汉式,他们的区别在于:
    懒汉式:指全局的单例实例在第一次被使用时构建。
    饿汉式:指全局的单例实例在类装载时构建。
    从它们的区别也能看出来,日常我们使用的较多的应该是懒汉式的单例,毕竟按需加载才能做到资源的最大化利用嘛。

综上所述,一个好的单例模式的实现需要满足如下条件:
     1.在调用getInstance()方法时返回一个且唯一的Singleton对象;
     2.能够在多线程使用时也能保证获取的Singleton对象唯一;
     3.getInstance()方法的性能要保证;
     4.能在需要的时候才初始化,否则不用初始化。

二、单例(Singleton)模式的实现(一)----饿汉式
      下述为饿汉式的写法,满足了上边说的第1,2条要求。该模式有几点要注意:
      1.默认构造方法需要私有化,不然外部可以随时的构造方法,这样就没法保证单例了。
      2.SingletonClass 类型的静态变量instance也是私有化的。这样外部就不能直接获取到instance,并且正是由于instance是静态变量并且声明时就初始化了,我们知道根据java虚拟机和类加载器(ClassLoader)的特性,一个类在一个类加载器(ClassLoader)中只会被加载一次。并且这里的instance在加载时就已经初始化了,这可以确定对象的唯一性。也就是说保证了在多线程并发情况下获取到的对象是唯一的。
      当然这种方式肯定也是有缺点的,就是不能满足上边要求中的第三点,例如某类实例需求依赖在运行时的参数来生成,那么由于饿汉式在类加载时就已经初始化了,所以无法满足懒加载。

/**  
  * 基于ClassLoader的机制,在同一ClassLoader下,该方式可以解决多线程同步的问题, 但是这种单例模式没有办法实现懒加载 
  */
public class SingletonClass {
  /**     
   * 在ClassLoader加载该类时,就会初始化instance     
   */
  private static SingletonClass instance = new SingletonClass();
    
  private SingletonClass() {    
  }
    
   public static SingletonClass getInstance() {
       return instance;    
   }
}

补充:什么时候是类装载时?
不严格的说,大致有这么几个条件会触发一个类被加载:
    a. new一个对象时;
    b. 使用反射创建它的实例时;
    c. 子类被加载时,如果父类还没被加载,就先加载父类;
    d. jvm启动时执行的主类会首先被加载。

三、单例(Singleton)模式的实现(二)----懒汉式(非线程安全)
      下述为懒汉式(非线程安全)的写法,可以看出确实是在调用getInstanceUnLocked()方法时,才会初始化实例,实现了懒加载。分析下能否满足在多线程下正常工作:我们在这里先分析一下假设有两个线程ThreadA和ThreadB:
      ThreadA首先执行到line1,这时instance为null,ThreadA将接着执行new SingletonLazy();在这个过程中如果instance已经分配了内存地址,但是还没有完成初始化工作(问题就出在这儿,稍后分析),如果ThreadB执行了line1,因为instance已经指向了某一内存,所以将跳过new SingletonLazy()直接得到instance,但是此时instance还没有完成初始化,那么问题就出现了。造成这个问题的原因就是new SingletonLazy()这个操作不是原子操作。至少可以分解成以下上个原子操作:
             1.分配内存空间
             2.初始化对象
             3.将对象指向分配好的地址空间(执行完之后就不再是null了)
     其中第2,3步在一些编译器中为了优化单线程中的执行性能是可以重排的。重排之后就是这样的:
            1.分配内存空间
            2.将对象指向分配好的地址空间(执行完之后就不再是null了)
            3.初始化对象
     重排之后就有可能出现上边分析的情况。

 /**
  * 只有在getInstance()时才会初始化instance 
  */
public class SingletonLazy {
    private static SingletonLazy instance;
         
    private SingletonLazy() {    
    }    
    
    public static SingletonLazy getInstanceUnLocked() {
        //line1        
        if (instance == null) {
             //line2
            instance = new SingletonLazy();        
        }
             
        return instance;
    }
}

四、单例(Singleton)模式的实现(三)----懒汉式(线程安全)
      下述为懒汉式(线程安全)的写法,和线程不安全的懒加载方式就是多了一个synchronized关键字,保证了线程安全,但是这又带来了另外一个问题,性能问题。如果,有多个线程会频繁调用getInstanceLocked()方法的话,可能会造成很大的性能损失。当然如果没有多线程频繁调用的话,就不存在多少性能损失了。

/**
 * 只有在getInstance()时才会初始化instance  
 */
public class SingletonLazy {    
    private static SingletonLazy instance;    
    
    private SingletonLazy() {       
    }

    /**
     * 方法名多了Locked表示是线程安全的,没有其他意义 
     */    
    public synchronized static  SingletonLazy getInstanceLocked() {
        if (instance == null) {
            instance = new SingletonLazy();        
        }        
              
        return instance;    
    } 
}

五、单例(Singleton)模式的实现(四)----双重检查锁定(简称DCL)
      下述为双重检查锁定的写法,这个方法也是有问题的,而这个问题和上边介绍过的重排问题一样。
      还是举ThreadA和ThreadB的例子:当Thread经过第一次检查对象为null时,会接着去加锁,然后去执行new SingletonLazy(),上边已经分析过了,改步骤存在重排现象,如果发生重排,即instance分配了内存地址,但是很没有完成初始化工作,而此时ThreadB,刚好执行第一次检查(没有加锁),instance已经分配了地址空间,不再为null,那么ThreadB会获取到没有完成初始化的instance,这就出现了问题。当然方法还是有的,那就是volatile关键字(作用是
禁止指令重排)。在JDK1.5之后使用volatile关键字,将禁止上文中的三步操作重排,既然不会重排,也就不会出现问题了。

/**
 * 双重检查锁定DCL  
 */
public class SingletonLazy {
    private static SingletonLazy instance;    
    
    private SingletonLazy() {    
    }    
    
    public static SingletonLazy getInstance() {
        if (instance == null) {//第一次检查            
            synchronized (SingletonLazy.class) {//加锁
                if (instance == null) {//第二次次检查                    
                    instance = new SingletonLazy();//new 一个对象
                }            
            }       
        }    
    
        return instance;    
    } 
}

优化:

/**
 * 双重检查锁定DCL (优化)
 */
public class SingletonLazy {
    private volatile static SingletonLazy instance;    
    
    private SingletonLazy() {    
    }    
    
    public static SingletonLazy getInstance() {
        if (instance == null) {//第一次检查            
            synchronized (SingletonLazy.class) {//加锁
                if (instance == null) {//第二次次检查                    
                    instance = new SingletonLazy();//new 一个对象
                }            
            }       
        }    
    
        return instance;    
    } 
}

六、单例(Singleton)模式的实现(五)----静态内部类
      下述为静态内部类的写法,这是一个很聪明的方式,结合了结合了饿汉式和懒汉式的优点,并且也不影响性能。因为我们在单例类SingletonInner类中,实现了一个static的内部类SingletonInnerHolder,该类中定义了一个static的SingletonInner类型的变量instance,并且会在ClassLoader第一次加载SingletonInnerHolder这个类时进行初始化。这样做的好处是在ClassLoader在加载单例类SingletonInner时不会初始化instance。只有在第一次调用SingletonInner的getInstance()方法时,ClassLoader才会去加载SingletonInnerHolder,并初始化instance,并且由于ClassLoader的机制,一个ClassLoader同一个类,只加载一次,那么不管多少线程,得到的也是同一个类,保证了并发下是该方式是可用的。其缺点也是有的,有些语言不支持这种语法。

/**
 * 静态内部类方式实际上是结合了饿汉式和懒汉式的优点的一种方式  
 */
public class SingletonInner {
    private SingletonInner() {    
    }
      
    /**
     * 在调用getInstance()方法时才会去初始化mInstance,实现了懒加载         
     */
    public static SingletonInner getInstance() {
        return SingletonInnerHolder.instance;
    }    
    
    /**
     * 静态内部类。因为一个ClassLoader下同一个类只会加载一次,保证了并发时不会得到不同的对象        */
    public static class SingletonInnerHolder {
        public static SingletonInner instance = new SingletonInner();
    }
}

七、单例(Singleton)模式的实现(六)----枚举
      下述为枚举的写法,不仅能避免多线程并发同步的问题,而且还天生支持序列化,可以防止在反序列化时创建新的对象。是一种比较推荐的方式,在java中需要在JDK1.5以上才支持enum。
      获取资源的方式很简单,只要 SingletonEnum .SINGLETON_ENUM.getInstance() 即可获得所要实例。

public enum SingletonEnum {
    SINGLETON_ENUM;    
    
    private Singleton singleton;
    private SingletonEnum() {
        singleton= new Singleton ();
    }         

    public Singleton getInstance() {
        return singleton;
    }
}

八、单例(Singleton)模式的总结

      单例模式还有其他的实现方法,在上述介绍的各种方式中,没有哪一个是绝对最好的,需要结合各自的情况决定。例如一般不要求懒加载的话,可以使用写法一饿汉式。如果要求懒加载,如果明确需要懒加载的,再根据是否需要线程安全考虑选择写法二,三。如果单例类需要反序列化,那么可以使用写法六枚举。总之,需要结合自己的实际情况来看。最后,再来看看几个问题:
      第一 、多ClassLoder情况,如果是多个ClassLoder都加载了单例类,那么就会出现多个同名的对象,这违背了单例模式的原则。解决这个问题,就要保证只有一个ClassLoder加载单例类。
      第二、单例类序列化问题,只要保证反序列化时,得到同一个对象就可以了,通过重写readResolve()方法可以实现。

public class Singleton implements java.io.Serializable {
    ...
    private Object readResolve() {
        return mInstance;        
    }
    ...    
}

 

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页