【设计模式 二】单例模式
定义
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
动机
为什么使用单例模式
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。
为什么不使用全局变量
1,全局变量可以实现全局访问,但是它不能保证应用程序中只有一个实例
2,编码规范建议少用全局变量
3,全局变量不能实现继承
实现方式
以下内容来自http://blog.csdn.net/jason0539/article/details/23297037/
懒汉式单例
线程不安全
package 设计模式;
/** * 懒汉式单例模式,在第一次调用方法时实例化自己 * * @author 田茂林 * @data 2017年9月1日 下午3:29:47 */
public class Singlenton {
private static Singlenton single;
private Singlenton() {
};
//静态工厂方法
public static Singlenton getInstance() { //线程不安全写法
if (single == null) {
single = new Singlenton();
}
return single;
}
}
Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例
线程安全
方法添加同步块的方式
package 设计模式;
/** * 懒汉式单例模式,在第一次调用方法时实例化自己 * * @author 田茂林 * @data 2017年9月1日 下午3:29:47 */
public class Singlenton {
private static Singlenton single;
private Singlenton() {
};
//静态工厂方法
public static synchronized Singlenton getInstance() { //线程安全写法,加了同步字段synchronized
if (single == null) {
single = new Singlenton();
}
return single;
}
}
在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的
升级版:双重检查锁定(双边校验锁定)
package 设计模式;
/** * 懒汉式单例模式,在第一次调用方法时实例化自己 * * @author 田茂林 * @data 2017年9月1日 下午3:29:47 */
public class Singlenton {
private static Singlenton single;
private Singlenton() {
};
//静态工厂方法
public static Singlenton getInstance() { //线程安全写法,加了同步字段synchronized
if (single == null) {
//在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
synchronized (Singlenton.class) {
if(single==null){
single = new Singlenton();
}
}
}
return single;
}
}
为什么要使用双重检查锁定呢?(第二重single==null的作用)
考虑这样一种情况,就是有两个线程同时到达,即同时调用 getInstance() 方法,此时由于 single == null ,所以很明显,两个线程都可以通过第一重的 singleTon == null ,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleTon == null ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new SingleTon()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,此时,如果没有第二重 singleTon == null 的话,那么第二个线程还是可以调用 new SingleTon ()语句,这样第二个线程也会创建一个 SingleTon实例,这样也还是违背了单例模式的初衷的。
为什么要设置第一重(第一重single==null的作用)
例子2:指令重排导致单例模式失效
我们都知道一个经典的懒加载方式的双重判断单例模式:
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance == null) {
synchronzied(Singleton.class) {
if(instance == null) {
**instance = new Singleton()**; //非原子操作
}
}
}
return instance;
}
}
但是锁住的实例对象必须初始化!
首先应该注意的是使用内置锁加锁的是Singleton.class,并不是instance,也就是说没有在instance实现同步,那么在这种情况下,当有两个线程同时进行到synchronized代码块时,只有一个线程可以进入,然后初始化了instance,但是这仅仅只能保证的是两个线程在访问上的独占性,也就是说两个线程在此一定是一先一后进行访问,但是不能保证的是instance的内存可见性,原因很简单,因为同步的对象并不是instance,而是Singleton.class(可以保证内存可见性)。
看似简单的一段赋值语句:instance= new Singleton(),但是很不幸它并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
超级版,静态内部类
package 设计模式;
/** * 懒汉式单例模式,在第一次调用方法时实例化自己 * * @author 田茂林 * @data 2017年9月1日 下午3:29:47 */
public class Singlenton {
private Singlenton() {
};
//静态工厂方法
private static class SinglentonHolder{
private static final Singlenton single = new Singlenton();
}
public static final Singlenton getInstance() {
return SinglentonHolder.single;
}
}
这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
既可以实现延迟加载:如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化
又可以实现线程安全:因为也是在类加载过程中得到实例化对象的,只不过不是Singleton类加载时,而是SingletonHolder类加载时实现的。
TML目录分割
饿汉式单例
package 设计模式;
/** * 饿汉式单例模式,在类加载时得到自己的实例 * * @author 田茂林 * @data 2017年9月1日 下午3:29:47 */
public class Singlenton {
private static Singlenton single = new Singlenton();
private Singlenton() {
};
public static Singlenton getInstance() {
return single;
}
}
在类创建的时候就已经同时实例化好了对象,所以天生线程安全
顽强壁垒:枚举单例
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
对于序列化和反序列化,因为每一个枚举类型和枚举变量在JVM中都是唯一的,即Java在序列化和反序列化枚举时做了特殊的规定,枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject会破坏单例的问题。
应用场景
- Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。