什么是单例
单例是指只会初始化一次,因而最多只会有一个实例的类。单例一般用来表示本质上只有一个的组件。比如操作系统中的窗体管理器和文件系统等。
单例类具备哪些要求,详细看蓝鸥上海Java培训的分享
在使用单例时,需要考虑以下几点:
l 访问权限控制,应当使用私有属性或方法生成实例
l 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)§
l 反序列化问题(防止多次反序列化生成多个不同的实例)
l 线程安全(防止不同线程生成多个不同的实例)
l 是否使用延迟加载,只在需要的时候才生成实例
l 如果不考虑延迟加载的问题,枚举是实现单例的最佳选择。
下面以一个完整的例子讲解在不使用枚举的情况下,做到以上几点(除了反射攻击)。
访问权限控制
例子中成员变量singleton和构造器都是私有类型的,实现了访问权限控制。
解决反射攻击问题(无除枚举外的其它方式)
目前除了使用枚举似乎没有其它方法可以解决反射攻击,以下代码仍然无法避免反射攻击。
这段代码本意是,添加成员变量initialized,用来标识是否生成过实例,在调用构造函数时,如果已经调用过一次,生成过实例,则报错。但如果使用反射的方式先将initialized改为false,再调用私有构造函数,就可以顺利绕过initialized,生成第二个实例,破坏单例性。
解决反序列化问题
TantanitLogo类实现了Serializable接口,可以被序列化和反序列化,为类添加的readResolve方法,可以解决反序列化时生成新的实例的问题。
在TantanitLogoTest类添加以下测试代码:
当TantanitLogo类中有readResolve方法时,ObjectInputStream的readObject方法会调用readResolve方法,所以输出结果为“tantanitLogo1与tantanitLogo3是同一个实例”,当TantanitLogo类中没有readResolve方法时,则输出“tantanitLogo1与tantanitLogo3不是同一个实例”。
线程安全
在上一篇文章,高效Java技巧之使用静态工厂方法代替构造器中讲解了使用静态工厂方法代替构造器的好处,这里就是使用getInstance方法就是代替构造器,生成实例。而使用synchronized关键字达到线程安全的目的,您可能注意到,我在synchronized代码块外加了singleton == null的条件判断。这是由于只有当singleton为null时才会进行new操作,生成新的实例,所以只在这个时候对代码加同步限制。
延迟加载
以下是TantanitLogo中的另一个静态类方法:
由于只在TantanitLogo的静态方法getInstance中进行new操作,生成新的实例。所以调用其它静态方法不会生成新的实例。在TantanitLogoTest添加方法进行测试:
输出结果为
满足延迟加载的要求。
您可以以每次启动调用一个测试函数的方法,对以上几个特征分别进行测试。如果您觉得哪个特征在您的应用场景中不重要,也可以很容易地进行简化。
使用枚举类实现单例
使用只有一个元素的枚举类可以很方便地实现单例,并且满足除了延迟加载之外的所有要求:
l 反序列化问题(防止多次反序列化生成多个不同的实例)
l 线程安全(防止不同线程生成多个不同的实例)