什么是单例
单例是指只会初始化一次,因而最多只会有一个实例的类。单例一般用来表示本质上只有一个的组件。比如操作系统中的窗体管理器和文件系统等。
单例类具备哪些要求
在使用单例时,需要考虑以下几点:
- 访问权限控制,应当使用私有属性或方法生成实例
- 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
- 反序列化问题(防止多次反序列化生成多个不同的实例)
- 线程安全(防止不同线程生成多个不同的实例)
- 是否使用延迟加载,只在需要的时候才生成实例
如果不考虑延迟加载的问题,枚举是实现单例的最佳选择。
下面以一个完整的例子讲解在不使用枚举的情况下,做到以上几点,(除了反射攻击)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class TantanitLogo implements Serializable { private volatile static TantanitLogo singleton; private volatile static boolean initialized = false;
private TantanitLogo() { synchronized (TantanitLogo.class) { if (initialized == false) { initialized = true; } else { throw new RuntimeException("受到反射攻击!"); } }
}
public static TantanitLogo getInstance() { if (singleton == null) { synchronized (TantanitLogo.class) { if (singleton == null) { singleton = new TantanitLogo(); } } } return singleton;
}
private Object readResolve() { return singleton; }
public static void otherMethod() { System.out.println("执行TantanitLogo类的静态方法otherMethod!"); if (TantanitLogo.singleton == null) { System.out.println("此时singleton为null,未被初始化!"); } else { System.out.println("此时singleton不为null,已被初始化!"); } } }
|
访问权限控制
例子中成员变量singleton和构造器都是私有类型的,实现了访问权限控制。
解决反射攻击问题(无除枚举外的其它方式)
目前除了使用枚举似乎没有其它方法可以解决反射攻击,以下代码仍然无法避免反射攻击。
1 2 3 4 5 6 7 8 9 10 11 12
| private volatile static boolean initialized = false;
private TantanitLogo() { synchronized (TantanitLogo.class) { if (initialized == false) { initialized = true; } else { throw new RuntimeException("受到反射攻击!"); } }
}
|
这段代码本意是,添加成员变量initialized,用来标识是否生成过实例,在调用构造函数时,如果已经调用过一次,生成过实例,则报错。但如果使用反射的方式先将initialized改为false,再调用私有构造函数,就可以顺利绕过initialized,生成第二个实例,破坏单例性。
解决反序列化问题
TantanitLogo类实现了Serializable接口,可以被序列化和反序列化,为类添加的readResolve方法,可以解决反序列化时生成新的实例的问题。
1 2 3
| private Object readResolve() { return singleton; }
|
在TantanitLogoTest类添加以下测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public static void testDeserialize() throws IOException, ClassNotFoundException { TantanitLogo tantanitLogo1 = TantanitLogo.getInstance();
FileOutputStream fos = new FileOutputStream("object.out"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(tantanitLogo1); oos.close(); fos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.out")); TantanitLogo tantanitLogo3 = (TantanitLogo) ois.readObject(); if (tantanitLogo1 == tantanitLogo3) { System.out.println("tantanitLogo1与tantanitLogo3是同一个实例"); } else { System.out.println("tantanitLogo1与tantanitLogo3不是同一个实例"); } }
|
当TantanitLogo类中有readResolve方法时,ObjectInputStream的readObject方法会调用readResolve方法,所以输出结果为“tantanitLogo1与tantanitLogo3是同一个实例”,当TantanitLogo类中没有readResolve方法时,则输出“tantanitLogo1与tantanitLogo3不是同一个实例”。
线程安全
1 2 3 4 5 6 7 8 9 10 11
| public static TantanitLogo getInstance() { if (singleton == null) { synchronized (TantanitLogo.class) { if (singleton == null) { singleton = new TantanitLogo(); } } } return singleton;
}
|
在上一篇文章中讲解了使用静态工厂方法代替构造器的好处,这里就是使用getInstance方法就是代替构造器,生成实例。而使用synchronized关键字达到线程安全的目的,您可能注意到,我在synchronized代码块外加了singleton == null的条件判断。这是由于只有当singleton为null时才会进行new操作,生成新的实例,所以只在这个时候对代码加同步限制。
延迟加载
以下是TantanitLogo中的另一个静态类方法:
1 2 3 4 5 6 7 8
| public static void otherMethod() { System.out.println("执行TantanitLogo类的静态方法otherMethod!"); if (TantanitLogo.singleton == null) { System.out.println("此时singleton为null,未被初始化!"); } else { System.out.println("此时singleton不为null,已被初始化!"); } }
|
由于只在TantanitLogo的静态方法getInstance中进行new操作,生成新的实例。所以调用其它静态方法不会生成新的实例。在TantanitLogoTest添加方法进行测试:
1 2 3 4 5 6
|
public static void testLazyLoad() { TantanitLogo.otherMethod(); }
|
输出结果为
1 2
| 执行TantanitLogo类的静态方法otherMethod! 此时singleton为null,未被初始化!”
|
满足延迟加载的要求。
您可以以每次启动调用一个测试函数的方法,对以上几个特征分别进行测试。如果您觉得哪个特征在您的应用场景中不重要,也可以很容易地进行简化。
使用枚举类实现单例
1 2 3
| public enum TantanitLogoEnum { singleton }
|
使用只有一个元素的枚举类可以很方便地实现单例,并且满足除了延迟加载之外的所有要求:
- 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
- 反序列化问题(防止多次反序列化生成多个不同的实例)
- 线程安全(防止不同线程生成多个不同的实例)
下篇文章,将会对Enum类的工作原理进行解析,并解释为什么枚举具有这些优势。