一、单例模式介绍
单例模式模式在程序的设计领域被广泛使用,例如设计一个单例模式对象来读取配置文件信息等。单例模式的主要特点是在内存中只存在一份对象,该对象的生命周期从创建到应用的结束。
其中单例模式又分为懒汉式以及饿汉式的单例模式,他们各自有各自的优缺点,具体使用哪种方式需要根据对象的特点来做出选择。懒汉式的单例模式使用的是时间换空间,其只在第一次使用的时候创建对象,因此,使用时创建对象需消耗
时间性能。饿汉式使用的是空间换
时间,在类第一次被装在时就创建对象,不管有没有使用该对象,因此在以后的访问中提高了对象的访问性能。下面就各种单例模式的写法做出归纳总结:
二、非线程安全的单例模式
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 single = new Singleton(); 15 } 16 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中创建单例模式的总结。