单例设计模式是一种比较简单的设计模式,涉及到的内容主要是static、synchronized、volatile关键字、内部类、对象克隆、序列化、枚举类型、反射和类加载机制等。
从表面上看,Singleton希望并限制该类的实例只能有一个,主要是由该类的构造方法的通常是private构造方法、static该实例以及返回该实例的getInstance方法。
下面将从多种单例的实现进行解说:
1、饿汉式单例
1 public class EagerSingleton{ 2 private static final EagerSingleton INSTANCE = new EagerSingleton(); 3 4 private EagerSingleton(){ 5 6 } 7 8 public static EagerSingleton getInstance(){ 9 return INSTANCE; 10 } 11 }
这种设计模式称谓“饿汉式”,顾名思义,该实例在类加载的时候就会自动创建,不管是不是被使用。所以如果该类的实例化的开销比较大,则该方式并不是最有的实现方法。优点是:无需担心锁线程同步获取该实例时可能出现的并发问题。
优点:1、线程安全2、在类加载的同时已经创建好了一个静态对象,调用时反应速度快。
缺点:1、资源效率不高,可能getInstance()永远不会执行到。
2、懒汉式单例
1 public class LazySingleton{ 2 private volatile static LazySingleton INSTANCE = null; 3 4 private LazySingleton(){ 5 } 6 7 public static synchronized LazySingleton getInstance(){ 8 if(INSTANCE == null){ 9 INSTANCE = new LazySingleton(); 10 }
11 return INSTANCE;
12 } 13 }
这种方式也叫做“懒汉式”单例,只有在使用时次进行实例化,避免多线程在并发场景下可能导致的多创建出一个实例的弊端,getInstance方法必须要加上synchronized方法或者采用synchroized代码块来加锁实现,但是这种过度保护的代价非常昂贵,只要是在该实例未被创建时才有必要进行加锁控制并发,因此更多的时候是没有必要同步的,此方法并发经济划算。
3、Lazy Signleton with Double Check
1 public class LazySingletibWithDoubleCheck{ 2 private volatile static LazySingletonWithDoubleCheck INSTANCE = null; 3 4 private LazySingletonWithDoubleCheck(){ 5 6 } 7 8 public static LazySingletibWithDoubleCheck getInstance(){ 9 if(INSTANCE == null){ 10 synchronized (LazySingletibWithDoubleCheck.class){ 11 if(INSTANCE == null){ 12 INSTANCE = new LazySingletibWithDoubleCheck(); 13 } 14 } 15 } 16 return INSTANCE; 17 } 18 }
是LazySingleton的改良版本,采用double-check的实现方式避免了对getInstance方法加锁。在实例尚未被实例化时,存在两次检查的流程,第一次检查如果实例已经存在就可以直接返回,反之则进行第二次检查,原因在于可能出现多个线程同时通过了第一次检查,此时必须通过加锁机制实现真正实例化时的排他性,保证只有一个线程成功抢占到锁并执行。目前该方法已经被广泛弃掉,替代的是Lazy initialization holder。
4、Inner Class Singleton
1 public class InnerClassSingleton{ 2 private static class SingletonHolder{ 3 private static final InnerClassSingleton INSTANCE = new InnerClassSingleton{ 4 } 5 } 6 7 private InnerClassSingleton(){ 8 } 9 10 public static final InnerClassSingleton getInstance(){ 11 return SingletonHolder.INSTANCE; 12 } 13 }
静态内部类的静态常量实例化,INSTANCE是常量,因此只能赋值一次,而且它是静态的,因此随着内部类一块加载。该写法使用了JVM本身机制保证了线程的安全;由于InnerClassSIngleton是私有的,除了getInstance()之外没有办法访问到它,因此它是懒汉式;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK的版本。
以上几种方法是常见的Singleton的方式。如何保证全局的唯一性,如何通过一些手段方法去破坏这唯一性,单纯通过绕开传统Singleton实现中仅靠将构造函数私有化达成的单例从而创建出多个实例。
1、Break Signleton with Clonable
1 public class CloneableSingleton implements Cloneable{ 2 private static final CloneableSingleton INSTANCE = new CloneableSingleton(); 3 4 private CloneableSingleton(){ 5 } 6 7 public static CloneableSingleton getInstance(){ 8 return INSTANCE; 9 } 10 11 public Object clone() throws CloneNotSupportedException{ 12 return super.clone(); 13 } 14 15 }
第一中方法是通过clone的方式破坏单例的唯一性;Java中类通过实现Cloneable接口并复写clone方法就可以完成一个对象的拷贝。可以通过一下代码进行破坏单例。
1 public static void checkClone() throws Exception{ 2 CloneableSingleton a = CloneableSingleton.getInstance(); 3 CloneableSingleton b = (CloneableSingleton) a.clone(); 4 5 assertEquals(a, b); 6 7 }
2、Break Singleton with Serialization
1 public class SerializableSingleton implements Serializable{ 2 3 private static final long serialVersionUID = 3506845333451732997L; 4 5 private static final SerializableSingleton INSTANCE = new SerializableSingleton(); 6 private SerializableSingleton(){ 7 8 } 9 10 public static SerializableSingleton getInstance(){ 11 return INSTANCE; 12 } 13 }
第二种方法是利用序列化与反序列化,当Singleton类实现了Serializable接口就代表它是可以被序列化的,该实例会被保存在文件中,需要是从该文件中读取并反序列化成对象。默认的反序列化过程是绕开构造函数直接使用字节生成一个新的对象,于是Singleton在反序列化时被创造出第二个实例。通过以下代码可以实现这一行为。
1 public static void checkSerialization() throws Exception{ 2 File file = new File("SerializableSingleton.out"); 3 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); 4 SerializableSingleton a = SerializableSingleton.getInstance(); 5 out.writeObject(a); 6 out.close(); 7 8 ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); 9 SerializableSingleton b = (SerializableSingleton)in.readObject(); 10 in.close(); 11 12 assertEquals(a, b); 13 }
3、Break Singleton with Reflection
1 public static void checkReflection() throws Exception{ 2 EagerSingleton a = EagerSingleton.getInstance(); 3 Constructor<EagerSingleton> cons = EagerSingleton.class.getDeclaredConstructor(); 4 cons.setAccessible(true); 5 EagerSingleton b = (EagerSingleton)cons.newInstance(); 6 assertEquals(a, b); 7 }
前面两种破坏方式是通过避开私有构造函数另辟蹊径来实现的,而将私有不允许外界直接调用的,通过反射机制强行公开起访问权限。
4、Break Singleton with Classloaders
1 public static void checkClassloader() throws Exception{ 2 String className = "fernando.lee.singleton.EagerSingleton"; 3 ClassLoader classLoader1 = new MyClassloader(); 4 Class<?> class1 = classLoader1.loadClass(className); 5 6 ClassLoader classLoader2 = new MyClassloader(); 7 Class<?> class2 = classLoader2.loadClass(className); 8 9 System.out.println("classLoader1" + class1.getClassLoader()); 10 System.out.println("classLoader2" + class2.getClassLoader()); 11 12 Method getInstance1 = class1.getDeclaredMethod("getInstance"); 13 Method getInstance2 = class2.getDeclaredMethod("getInstance"); 14 15 Object a = getInstance1.invoke(null); 16 Object b = getInstance2.invoke(null); 17 18 assertEquals(a, b); 19 }
Java中一个类并使单纯依靠其包类名来标记的,而是全包类名加上加载它的类加载器共同确定的。因此不同的类加载器加载的Singleton类并不认为是相同的,所以单例会被破坏,通过自定义编写的MyClassLoader即可实现。
由此看来,Singleton唯有妥善关闭上述的几个后门才能称得上是真正的单例。目前只了解到有两种对应措施:第一方式完善现有的实现让克隆、序列化、反射和类加载器无从下手,第二种则是通过枚举类型间接实现单例。
1、Safe Singleton
1 public class SafeSingleton implements Serializable, Cloneable{ 2 3 private static final long serialVersionUID = -3789939603862706007L; 4 5 private static SafeSingleton INSTANCE = new SafeSingleton(); 6 7 private SafeSingleton(){ 8 if (INSTANCE != null){ 9 throw new IllegalStateException("Singleton instance Already created."); 10 } 11 } 12 13 private static SafeSingleton getInstance(){ 14 return INSTANCE; 15 } 16 17 private Object readResolve() throws ObjectStreamException{ 18 return INSTANCE; 19 } 20 21 public Object clone() throws CloneNotSupportedException{ 22 throw new CloneNotSupportedException("Singleton can't be cloned"); 23 } 24 }
在原有的Singleton的基础上完善若干方法即可实现一个更安全的更为纯正的Singleton。当实例已经存在时试图通过调用私有构造函数会直接报错,直接抵御了反射机制的入侵;让调用clone方法直接报错避免了实例被克隆;复写readReslove方法直接返回现有的实例本身可以防止反序列化过程中生成新的实例。
2、Enum Singleton
1 public enum EnumSingleton{ 2 3 YuLe("T39487342342344", "YuLe", "娱乐"), 4 KeJi("T45345345345345", "KeJi", "科技"), 5 TiYu("T53453242435455", "TiYu", "体育"); 6 7 private String type; 8 private String eName; 9 private String desc; 10 11 private EnumSingleton(String type, String eName, String desc){ 12 this.type = type; 13 this.eName = eName; 14 this.desc = desc; 15 } 16 17 public String getType() { 18 return type; 19 } 20 21 public void setType(String type) { 22 this.type = type; 23 } 24 public String getEname() { 25 return eName; 26 } 27 public void setEname(String eName) { 28 this.eName = eName; 29 } 30 public String getDesc() { 31 return desc; 32 } 33 public void setDesc(String desc) { 34 this.desc = desc; 35 } 36 37 public String toString(){ 38 return this.desc + " " + this.eName + " " + this.type; 39 } 40 }
采用枚举的方式实现的非常的简单,可以直接通过EnumSingleton.TuLe获取该实例。Java中所有定义为enum的类内部都继承了Enum类,而Enum具备的特性包括类加载是静态的来保证线程安全,而且其中clone方法是final的直接会抛出CloneNotSupportedException一场因而不具备拷贝,同时与生俱来的序列化机制也是直接交由JVM掌握的并不会创建出新的实例,此外Enum不能被显式实例化反射破坏也不起作用。缺陷就是既然继承了Enum类,就不可以再集成其他的类,这是由Java的单集成模式限制的。所以,除非是有非常有用,否则不会仅仅为了一个纯的Singleton就把class变成enum。