单例模式 (Singleton pattern ):确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。(Ensure a class has only one instance,and provide a global point of access to it.)
在Java中对象都是通过new
关键字来创建的(当然也可以通过对象复制、反射等),我们如何才能控制对象的创建呢?最简单的方式是我们将构造函数设置为私有(private
)访问权限,这样就禁止外部创建对象了。
public class Singleton {
// 限制产生多个对象
private Singleton() {}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
上面的这种方式有时被称为饿汉模式,也就是提前将实例创建好,无论是否需要调用。Singleton
类称为单例类,通过使用private
的构造函数确保在一个应用中只产生一个实例,并且是自行实例化的,其通用类图如下:
这种方式基于加载机制从而避免了多线程的同步问题,INSTANCE
在类装载时就实例化了。另外我们可以使用静态代码块,以及构造块来实现,达到的效果是一样的,不过有些繁琐而已。
简单叙述:static{…}是静态块,而只有{…}的是叫做构造块。
与饿汉模式相对应的是懒汉模式,顾名思义,也就是在生成单例类的实例时延迟加载,在第一次使用时(调用)才进行创建实例。
最简单的实现方式如下:
public class SingleTon {
private static SingleTon INSTANCE = null;
private SingleTon(){}
public static SingleTon getInstance(){
//返回实例的时候检查是否创建
if(null==INSTANCE){
INSTANCE=new SingleTon();
}
return INSTANCE;
}
}
这种方式可以实现我们的需求,但是在高并发的情况下会出问题,因为其存在线程问题:
针对上面的缺陷,我们进行如下优化:
public class SingleTon {
private static SingleTon INSTANCE = null;
private SingleTon(){}
public static synchronized SingleTon getInstance(){
//返回实例的时候检查是否创建
if(null==INSTANCE){
INSTANCE=new SingleTon();
}
return INSTANCE;
}
}
这种写法能够在多线程中很好的工作,但是,遗憾的是,效率很低,99%情况下不需要同步。
升级版-双重检查锁定(1.5之后的新特性)
public class SingleTon {
private volatile static SingleTon singleton;
private SingleTon() { }
public static SingleTon getSingleton() {
if (null == singleton) {
synchronized (SingleTon.class) {
if (null == singleton) {
singleton = new SingleTon();
}
}
}
return singleton;
}
}
懒汉与饿汉区别:
synchronized
则会导致对对象的访问不是线程安全的。servlet
容器对每个servlet
使用完全不同的类装载器,这样的话如果有两个servlet
访问一个单例类,它们就都会有各自的实例。Singleton
实现了java.io.Serializable
接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。Cloneable
接口,并且实现了clone方法,则可以直接通过对象复制方式创建一个对象,对象复制是不用调用类的构造函数,因此即使是私有的构造函数,对象仍然可以被复制。这种情况很少见,但是还是提出来比较好,解决该问题的最好方法是单例类不实现Cloneable
接口。这里了解另一种特殊的懒汉模式:静态内部类
public class SingleTon {
private static class SingletonHolder {
private static SingleTon INSTANCE = new SingleTon();
}
private SingleTon() {}
public static synchronized SingleTon getInstance() {
return SingletonHolder.INSTANCE;
}
}
看起来没有太大区别,但是在加载方式上的细微区别。前面所提及的几种方式只要Singleton
类被装载了,那么INSTANCE
就会被实例化,而静态内部类的方式是Singleton
类被装载了,INSTANCE
也不一定被初始化。因为SingletonHolder
类没有被主动使用,只有显示通过调用getInstance
方法时,才会显示装载SingletonHolder
类,从而实例化INSTANCE
。
这种方式同样利用了classloder
的机制来保证初始化INSTANCE
时只有一个线程。
想象一下,如果实例化INSTANCE
很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton
类加载时就实例化,因为我不能确保Singleton
类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化INSTANCE
显然是不合适的。这个时候,静态内部类的方式就显得很合理了。
我们都知道在实现对象的序列化的时候,我们只需要实现 Serializable
接口即可,这样我们就可以把对象往内存里写然后再从内存里读出而”组装”成一个跟原来一模一样的对象了。
不过当序列化遇到单例时,这里边就有了个问题: 从内存中读出而组装的对象破坏了单例的规则。单例是要求一个JVM中只有一个类对象的,而现在通过反序列化,一个新的对象克隆了出来。示例:
public final class MySingleton implements Serializable {
private MySingleton() { }
private static final MySingleton INSTANCE = new MySingleton();
public static MySingleton getInstance() { return INSTANCE; }
}
当把 MySingleton
对象(通过getInstance
方法获得的那个单例对象)序列化后再从内存中读出时, 就有一个全新的但跟原来一样的MySingleton
对象存在了。
那怎么来维护单例模式呢?这就要用到readResolve
方法了。 如下所示:
public final class MySingleton implements Serializable{
private MySingleton() { }
private static final MySingleton INSTANCE = new MySingleton();
public static MySingleton getInstance() { return INSTANCE; }
// 使用readResolve方法来保护保护单例属性
private Object readResolve() throws ObjectStreamException {
// Return the one true MySingleton and let the grabage collector
// take care of MySingleton impersonator
return INSTANCE;
}
}
这样当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个 readResolve
方法来返回我们指定好的对象了,单例规则也就得到了保证。
上面所提及的方式都可以在正常情况下正常使用,但是如果恶意的去利用反射机制完全可以将其破坏掉,从而不再是单例。
例如享有特权的客户端可以借助AccessibleObject.setAccessible
方法,通过反射机制来调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
下面我们实地操作一下,来破坏单例模式:
public class SingleTon {
public final static SingleTon INSTANCE = new SingleTon();
private SingleTon() {}
}
public class Ceshi_Singleton {
public static void main(String[] args) {
SingleTon s1 = SingleTon.INSTANCE;
SingleTon s2 = SingleTon.INSTANCE;
System.out.println("正常情况下单例模式执行情况:" + (s1 == s2));
SingleTon s3 = SingleTon.INSTANCE;
SingleTon s4 = destroySingleton();
System.out.println("非正常情况下单例模式执行情况:" + (s3 == s4));
}
// 破坏单例模式的方法
private static SingleTon destroySingleton() {
SingleTon returnSingleton = null;
try {
Class cls = SingleTon.class;
Constructor constructor = cls.getDeclaredConstructor(new Class[] {});
constructor.setAccessible(true);
returnSingleton = constructor.newInstance(new Object[] {});
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
return returnSingleton;
}
}
运行结果:
正常情况下单例模式执行情况:true
非正常情况下单例模式执行情况:false
对于这个结果很显然证明了我们成功的破坏了单例模式的唯一性,对于单例模式,是否很诧异呢?
JDK1.5 引入了枚举类型,借用这一新特性,我们可以用它来实现单例模式:
public enum SingleTon {
INSTANCE;
public void a(){...};
//...
}
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅更加简洁同时能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候。
虽然这种方法还没有广泛使用,但是单元素的枚举类型已经成为实现Singleton
的最佳方法。
参考资料: