首页 > 代码库 > 设计模式——单例模式

设计模式——单例模式

一、单例模式介绍

  单例模式模式在程序的设计领域被广泛使用,例如设计一个单例模式对象来读取配置文件信息等。单例模式的主要特点是在内存中只存在一份对象,该对象的生命周期从创建到应用的结束。

其中单例模式又分为懒汉式以及饿汉式的单例模式,他们各自有各自的优缺点,具体使用哪种方式需要根据对象的特点来做出选择。懒汉式的单例模式使用的是时间换空间,其只在第一次使用的时候创建对象,因此,使用时创建对象需消耗

时间性能。饿汉式使用的是空间换

时间,在类第一次被装在时就创建对象,不管有没有使用该对象,因此在以后的访问中提高了对象的访问性能。下面就各种单例模式的写法做出归纳总结:

二、非线程安全的单例模式

public class Singleton {	private static Singleton single;	private Singleton()	{	}	public static Singleton getInstance()	{		if(single == null)			single = new Singleton();		return single;	}}

 上面这段代码写的是一个懒汉式的单例模式,但是很显然,在多线程的环境下,这段代码是不安全的。因为在single = new Singleton()这个语句被翻译成机器码时会有多个执行指令。首先,会创建Singleton对象,其次调用其构造方法,实例化成员变量,最后将创建的对象的堆上地址复制给single变量。那么同事有多个线程在执行时,若其中有一个线程执行创建Singleton对象后还没有赋值给single变量,则下一个线程执行到if语句判断,这时single为null,则改线程同样会继续创建对象,因此,这段代码在多线程的情况下会产生多份实例。那么接下来我们通过加锁机制来实现线程安全的懒汉模式。

 

三、线程安全的懒汉模式

 1 public class Singleton { 2     private static Singleton single; 3  4     private Singleton() 5     { 6  7     } 8  9     public static synchronized Singleton getInstance()10     {11 12         if(single == null)13         {14 15         }16        single = new Singleton();17         return single;18     }19 20 }

       很简单,只要在方法前面加上synchronized关键字即可实现线程安全的单例模式,但是该方法也有一个很大的缺陷,锁的粒度太大,当多个线程同时调用该方法时,一次只能有一个线程调用该方法读取single对对象(特别是singleton对象已经创建了)。这种实现方式很影响程序的性能。那么下面我们需要对锁的粒度进行缩小,即在第一次创建single对象的时候对其进行加锁,以后对象创建后,我们不需要获取锁来保持互斥。这就是下面将要讲的双重锁检查。

四、懒汉模式(双重检查锁)

public class Singleton {	private static Singleton single;	private Singleton()	{	}	public static  Singleton getInstance()	{		if(single == null)		{			synchronized (Singleton.class) {				if(single == null)					single = new Singleton();			}		}		return single;	}}

    为什么上面的代码需要在synchronized同步块里面加上对single==null的判断?我们可以假设两个线程同时进入到第一个single==null的块内,此时有且仅有一个线程获取到同步锁,另一个线程等待,加入不在同步块里面加上single==null的判断,当第一个线程创建完对象离开同步块时,第二个线程进入同步块,此时它又创建了对象,这就导致了两个对象的创建。那么是不是双重检查锁就一定是线程安全的呢?当编译器对生成的指令做出优化或重排序时,这段代码还是存在问题。正如我们上面所说的single = new Singleton()对应了多个指令操作。通常情况下,编译器在不改变单线程的执行正确性的前提下,为提高程序的性能会给指令进行充排序。我们现在假设这样的一种场景发生的情况下将会产生非线程安全。

首先将single = new Singleton()拆分成三条指令。(1)对象创建。(2)调用构造函数,初始化实例变量 (3)将创建的对象地址赋值给single。

  现在编译器做了优化,使得指令的执行步骤为(1)(3)(2),因为这在单线程的情况下是不影响程序的正确性的,编译器可以做这种优化。当一个线程执行到指令(3)时,此时变量已经赋值。另外一个线程执行到第一个single == null的条件判断,发现single不等于null,则直接返回single但是此时的对象并没有初始化,因此其获取到的是一个未经初始化的对象,所以在这种情况下双重检查锁是非线程安全的。

那么怎么才能使其变成线程安全的呢,我们可以在single的声明中加上volatile关键字,即:

private static volatile Singleton single;

  volatile关键词的作用禁止编译器对指令进行重排序。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

其中需要注意的是在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即使将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

 

五、饿汉式

上面讲的几种方式都是与懒汉式相关的,下面我们来看看饿汉式的单例模式。

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

书写起来方面简单,唯一的缺点也是先前提到的在类装在时,便创建对象驻留在内存中,但是有点也是明显的。

 

六、静态内部类的方式。

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

这种方式使用了JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

 

 七、枚举方式

1 public enum EasySingleton{2 INSTANCE;3 }4 5  

简单,便捷。与此同时,它能够防止反序列化时创建新对象,其次线程安全,同时能够防止反射攻击。

以上就是在java中创建单例模式的总结。

 

 

设计模式——单例模式