定义
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
模式结构
单例模式使结构最简单的设计模式,它只包含一个类,即单例类。
优点
(1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2)由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
(3)允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。
缺点
(1)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
(3)Java语言提供了自动垃圾回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
适用环境
(1)系统只需要一个实例对象,例如系统要求提供一个唯一的序列号生成器或资源管理器,或者因为资源消耗太大而只允许创建一个对象。
(2)客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
单例模式的实现
饿汉式
(1)在成员变量声明处初始化
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
(2)在静态代码块中初始化
public class HungrySingleton {
private static final HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
上述两种代码本质上是相同的,都是在HungrySingleton加载时就完成了对单例对象的初始化,我们称之位饿汉式单例类
懒汉式
(1)未加锁
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {}
public static LazySingleton getInstance() {
if(null == lazySingleton) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
懒汉式注重的是延迟加载,只有在使用该对象时才会初始化该对象,但是上述代码存在线程安全问题。
(2)用synchronize关键字给静态方法加锁
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {}
public synchronized static LazySingleton getInstance() {
if(null == lazySingleton) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
当synchronize修饰在静态方法上时,会给这个LazySingleton.class加锁;当synchronize修饰在非静态方法上时,会给内存中的对象实例加锁。这个方法虽然解决了线程安全问题,但是每次用getInstance()都需要进行线程锁定判断,在多线程高并发访问环境中将会导致系统性能大大降低。
(3)双重检查锁定
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {}
public static LazyDoubleCheckSingleton getInstance() {
if(null == lazyDoubleCheckSingleton) {
synchronized (LazyDoubleCheckSingleton.class) {
if(null == lazyDoubleCheckSingleton) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
为什么要加volatile关键字?
在下面这行代码中,虽然看上去只有一行,但实际上可以拆分成3个步骤:
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
a:分配内存给这个对象
b:初始化这个对象
c:设置lazyDoubleCheckSingleton指向刚分配的内存地址
如果不加volatile关键字,b和c的顺序会因为Java虚拟机所做的一些代码优化而颠倒,使得真正的顺序成为a -> c -> b。Java虚拟机保证这个指令的重排序不会影响单线程的执行结果,但是不保证多线程的执行结果。
考虑下面这种情形:
分配对象的内存空间(线程一)
↓
设置instance指向内存空间(线程一)
↓
判断instance是否为null(线程二)
↓
线程二初次访问对象(线程二)
↓
初始化对象(线程一)
↓
线程一初次访问对象(线程一)
如上述流程所示,由于Java虚拟机的指令重排,当线程二访问对象时,线程一还没有完成对象的初始化,这就会出错。
饿汉式单例类与懒汉式单例类的比较
饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多个线程同时访问的问题,可以确保实例的唯一性;从调用速度和反应时间来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来将饿汉式单例不及懒汉式单例,并且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的几率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。
静态内部类
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
}
在类的初始化阶段,Java虚拟机会获取StaticInnerClassSingleton.class这个锁,这个锁可以保证多线程不受指令重排序的影响。
Java语言规定,发生下述5种情况时,类会被初始化:
有一个A类型的实例被创建。
有一个A类的静态方法被调用。
有一个A类的静态成员被赋值。
A类中声明的一个静态非常量成员被使用。
A类是一个顶级类,并且在类中有嵌套的断言语句。
防御序列化攻击
以上述饿汉式单例模式为例,一旦该类实现了Serializable接口,就需要增加readResolve()方法:
private Object readResolve() {
return hungrySingleton;
}
如果不增加readResolve()方法,通过序列化和反序列化的方式可以破坏单例模式。
防御反射攻击
没有任何一种单例模式能防御反射攻击,除了枚举类型。
枚举
public enum EnumInstance {
INSTANCE{
protected void printTest(){
System.out.println("print test");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
枚举类型实现单例模式是《Effective Java》这本书推荐的单例模式,既能防御序列化攻击,也能防御反射攻击。