单例模式(Singleton Pattern
)是 Java
中最简单的设计模式之一。这种类型的设计模式属于建立型模式。在 GOF
书中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。html
单例模式通常体如今类声明中,单例的类负责建立本身的对象,同时确保只有单个对象被建立。这个类提供了一种访问其惟一的对象的方式,能够直接访问,不须要实例化该类的对象。java
注意:
一、单例类只能有一个实例。
二、单例类必须本身建立本身的惟一实例。
三、单例类必须给全部其余对象提供这一实例。编程
优势:
一、在内存里只有一个实例,减小了内存的开销,尤为是频繁的建立和销毁实例(好比管理学院首页页面缓存)。
二、避免对资源的多重占用(好比写文件操做)。
缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。设计模式
单例模式的代码要素:
一、将构造函数私有化
二、在类的内部建立实例
三、提供获取惟一实例的方法缓存
//code 1 public class Singleton { //在类内部实例化一个实例 private static Singleton instance = new Singleton(); //私有的构造函数,外部没法访问 private Singleton() { } //对外提供获取实例的静态方法 public static Singleton getInstance() { return instance; } }
所谓饿汉。这是个比较形象的比喻。对于一个饿汉来讲,他但愿他想要用到这个实例的时候就可以当即拿到,而不须要任何等待时间。因此,经过static
的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton
的实例被建立出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。安全
同时,因为该实例在类被加载的时候就建立出来了,因此也避免了线程安全问题。(缘由见:在深度分析Java的ClassLoader机制(源码级别)、Java类的加载、连接和初始化)
还有一种饿汉模式的变种多线程
//code 3 public class Singleton2 { //在类内部定义 private static Singleton2 instance; static { //实例化该实例 instance = new Singleton2(); } //私有的构造函数,外部没法访问 private Singleton2() { } //对外提供获取实例的静态方法 public static Singleton2 getInstance() { return instance; } }
code 3
和code 1
实际上是同样的,都是在类被加载的时候实例化一个对象。并发
饿汉式单例,在类被加载的时候对象就会实例化。这也许会形成没必要要的消耗,由于有可能这个实例根本就不会被用到。并且,若是这个类被屡次加载的话也会形成屡次实例化。其实解决这个问题的方式有不少,下面提供两种解决方式,第一种是使用静态内部类的形式。第二种是使用懒汉式。app
//code 5 public class Singleton { //定义实例 private static Singleton instance; //私有构造方法 private Singleton(){} //对外提供获取实例的静态方法 public static Singleton getInstance() { //在对象被使用的时候才实例化 if (instance == null) { instance = new Singleton(); } return instance; } }
这段代码简单明了,并且使用了懒加载模式,可是却存在致命的问题。当有多个线程并行调用 getInstance()
的时候,就会建立多个实例。也就是说在多线程下不能正常工做。函数
//code 6 public class SynchronizedSingleton { //定义实例 private static SynchronizedSingleton instance; //私有构造方法 private SynchronizedSingleton(){} //对外提供获取实例的静态方法,对该方法加锁 public static synchronized SynchronizedSingleton getInstance() { //在对象被使用的时候才实例化 if (instance == null) { instance = new SynchronizedSingleton(); } return instance; } }
针对线程不安全的懒汉式的单例,其实解决方式很简单,就是给建立对象的步骤加锁。
这种写法可以在多线程中很好的工做,并且看起来它也具有很好的延迟加载,可是,遗憾的是,他效率很低,由于99%
状况下不须要同步。(由于上面的synchronized
的加锁范围是整个方法,该方法的全部操做都是同步进行的,可是对于非第一次建立对象的状况,也就是没有进入if
语句中的状况,根本不须要同步操做,能够直接返回instance
。)这就引出了双重检验锁。
//code 7 public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
针对上面code 6
存在的问题,相信对并发编程了解的同窗都知道如何解决。其实上面的代码存在的问题主要是锁的范围太大了。只要缩小锁的范围就能够了。那么如何缩小锁的范围呢?相比于同步方法,同步代码块的加锁范围更小。code 6
能够改形成上面的样子。
双重检验锁模式(Double Checked Locking Pattern
),是一种使用同步块加锁的方法。称其为双重检查锁,由于会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。为何在同步块内还要再检验一次?由于可能会有多个线程一块儿进入同步块外的if
,若是在同步块内不进行二次检验的话就会生成多个实例了。
可是,事情这的有这么容易吗?上面的代码看上去好像是没有任何问题。实现了惰性初始化,解决了同步问题,还减少了锁的范围,提升了效率。可是,该代码还存在隐患。隐患的缘由主要和Java内存模型(JMM)有关。
主要在于instance = new Singleton()
这句,这并不是是一个原子操做,事实上在JVM
中这句话大概作了下面 3 件事情。
给 instance 分配内存
调用 Singleton 的构造函数来初始化成员变量
将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
可是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序多是1-2-3
也多是1-3-2
。若是是后者,则在3
执行完毕、2
未执行以前,被线程二抢占了,这时instance
已是非null
了(但却没有初始化),因此线程二会直接返回instance
,而后使用,而后瓜熟蒂落地报错。
在J2SE 1.4
或更早的版本中使用双重检查锁有潜在的危险,有时会正常工做(区分正确实现和有小问题的实现是很困难的。取决于编译器,线程的调度和其余并发系统活动,不正确的实现双重检查锁致使的异常结果可能会间歇性出现。重现异常是十分困难的。) 在J2SE 5.0
中,这一问题被修正了。volatile
关键字保证多个线程能够正确处理单件实例。
因此,针对code 7 ,能够有code 8 和code 9两种替代方案:
//code 8 public class VolatileSingleton { private static volatile VolatileSingleton singleton; private VolatileSingleton() { } public static VolatileSingleton getSingleton() { if (singleton == null) { synchronized (VolatileSingleton.class) { if (singleton == null) { singleton = new VolatileSingleton(); } } } return singleton; } }
有些人认为使用 volatile
的缘由是可见性,也就是能够保证线程在本地不会存有 instance
的副本,每次都是去主内存中读取。但实际上是不对的。使用 volatile
的主要缘由是其另外一个特性:禁止指令重排序优化。也就是说,在 volatile
变量的赋值操做后面会有一个内存屏障(生成的汇编代码上),读操做不会被重排序到内存屏障以前。好比上面的例子,取操做必须在执行完 1-2-3
以后或者 1-3-2
以后,不存在执行到 1-3
而后取到值的状况。从「先行发生原则」的角度理解的话,就是对于一个 volatile
变量的写操做都先行发生于后面对这个变量的读操做(这里的“后面”是时间上的前后顺序)。
可是特别注意在 Java 5
之前的版本使用了 volatile
的双检锁仍是有问题的。其缘由是 Java 5
之前的 JMM
(Java
内存模型)是存在缺陷的,即时将变量声明成 volatile
也不能彻底避免重排序,主要是 volatile
变量先后的代码仍然存在重排序问题。这个 volatile
屏蔽重排序的问题在 Java 5
中才得以修复,因此在这以后才能够放心使用 volatile
。
上面这种双重校验锁的方式用的比较普遍,他解决了前面提到的全部问题。可是,即便是这种看上去天衣无缝的方式也可能存在问题,那就是遇到序列化的时候。详细内容后文介绍。
使用final
//code 9 class FinalWrapper<T> { public final T value; public FinalWrapper(T value) { this.value = value; } } public class FinalSingleton { private FinalWrapper<FinalSingleton> helperWrapper = null; public FinalSingleton getHelper() { FinalWrapper<FinalSingleton> wrapper = helperWrapper; if (wrapper == null) { synchronized (this) { if (helperWrapper == null) { helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton()); } wrapper = helperWrapper; } } return wrapper.value; } }
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
这种方式能达到双检锁方式同样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的状况,双检锁方式可在实例域须要延迟初始化时使用。
这种方式一样利用了 classloder
机制来保证初始化 instance
时只有一个线程,它跟第 1 种方式不一样的是:第 1 种方式只要 Singleton
类被装载了,那么 instance
就会被实例化(没有达到 lazy loading
效果),而这种方式是 Singleton
类被装载了,instance
不必定被初始化。由于 SingletonHolder
类没有被主动使用,只有经过显式调用 getInstance
方法时,才会显式装载 SingletonHolder
类,从而实例化 instance
。想象一下,若是实例化 instance
很消耗资源,因此想让它延迟加载,另一方面,又不但愿在 Singleton
类加载时就实例化,由于不能确保 Singleton 类还可能在其余的地方被主动使用从而被加载,那么这个时候实例化 instance
显然是不合适的。这个时候,这种方式相比第 1 种方式就显得很合理。
这种写法仍然使用JVM自己机制保证了线程安全问题;因为 SingletonHolder
是私有的,除了 getInstance()
以外没有办法访问它,所以它是懒加载的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK
版本。
// code 10 public enum Singleton { INSTANCE; Singleton() { } }
在Java 1.5
以前,实现单例通常只有以上几种办法,在Java 1.5
以后,还有另一种实现单例的方式,那就是使用枚举。能够经过Singleton.INSTANCE
来访问实例。
这种方式是Effective Java
做者Josh Bloch
提倡的方式(Effective Java
第3条),它不只能避免多线程同步问题,并且还能防止反序列化从新建立新的对象(下面会介绍),可谓是很坚强的壁垒啊,在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中有详细介绍枚举的线程安全问题和序列化问题。
那这种有啥好处?枚举的方式实现:
简洁
无尝提供了序列化机制
绝对防止屡次实例化,即便是在面对复杂的序列化或者反射攻击的时候(安全)!
这种也较为推荐使用!
有两个问题须要注意:
一、若是单例由不一样的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每一个servlet使用彻底不一样的类装载器,这样的话若是有两个servlet访问一个单例类,它们就都会有各自的实例。
该问题能够经过以下方式修复:
private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if(classLoader == null) classLoader = Singleton.class.getClassLoader(); return (classLoader.loadClass(classname)); } }
二、若是Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。无论怎样,若是你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。序列化问题参考下面的分析。
在单例与序列化的那些事儿一文中,分析过单例和序列化以前的关系——序列化能够破坏单例。要想防止序列化对单例的破坏,只要在Singleton类中定义readResolve就能够解决该问题。
//code 11 package com.hollis; import java.io.Serializable; /** * Created by hollis on 16/2/5. * 使用双重校验锁方式实现单例 */ public class Singleton implements Serializable{ 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; } private Object readResolve() { return singleton; } }
通常来讲,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章给出的第2种方法不算正确的写法。
通常状况下直接使用饿汉式就行了,若是明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,若是涉及到反序列化建立对象时,可使用枚举的方式来实现单例。若是有其余特殊的需求,能够考虑使用双检锁方式。
参考资料:
如何正确地写出单例模式
单例模式的七种写法
为何我墙裂建议你们使用枚举来实现单例
设计模式(二)——单例模式
单例模式
单例模式你会几种写法?