单例模式是编程经常会用到设计模式,什么是单例模式就不多说啦,但是单例模式使用不当就要尴尬了;
先看看一下我最早菜鸡式的单例模式:
public class Singleton { public Singleton() { } public static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } };
有一点编程经验的人一看就知道又是菜鸡在给自己挖坑啦;
- public 修饰 静态变量 instance 和 构造器, 这就给留下了几个严重的问题:1):外部类直接调用常量 instance 作为实例使用出错的bug;2):外部类完全可以通过new对象的方式获取实例,更不用说Java反射MagicPower,完全违背了单例设计原则:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例;
- 虽然有synchronize 关键字,但高并发的情况下并不是线程安全的,任然有可能发生指令重排带来是线程不安全问题;
对指令重排有不解的小伙伴可参考:java volatile关键字解惑
对症下药,经过改造后双重锁校验机制的单例:
public class Singleton { private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
修改:
- volatile 关键字修饰静态常量instance避免高并发下发生指令重排导致创建多个实例问题;
- private 关键字修饰的构造器避免外部类通过new对象方法实例对象;
存在的问题:
- 关键字synchronized,volatile带来的性能消耗问题;
- 说好的反射获取实例的问题;
先说问题1 另一个解决思路吧,那就是静态内部类单例模式了:
public class Singleton { private Singleton() { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
静态内部类:
在Singleton类加载时,内部静态类SingletonHolder并没有加载,只用调用getInstance()时才会执行加载SingletonHolder类中同时通过new Singleon()将实例化Singleton对象赋值给静态常量INSTANCE,,借助Java中static 常量属性(JVM机制)保证了Singleton的单例(线程安全),同时又避免了Java锁造成性能问题;但是Singleton类中有构造器,给强大的反射留下了可趁之机,所以理论上还是存在出现多个实例对象的现象;
其实静态内部类也是属于懒汉单例的变种,当需要的时候再去加载;
other:
最近看到一个“奇葩”的单例模式解决序列化存储数据,读出数据反序列引发的单例类多次实例化问题:具体描述和代码见分割线中
----------------------------------------------------分割线-------------------------------------------------------
不知道大家有没有想到过一个问题,那就是序列化。我们可以通过以下代码将实例写入磁盘,然后再从磁盘读出,即使构造方法是私有的,反序列化也是可以通过特殊的途径去重新创建一个新的实例,代码如下:
public Singleton createNewInstance() throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
// 此处的singleton为通过单例模式获取到的实例对象
oos.writeObject(singleton);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
// 此时返回一个反序列化后得到的新的实例对象
return (Singleton) ois.readObject();
}
可以通过上面的代码看到,反序列化后可以得到一个新的实例对象,那么这种现象没法避免了吗?其实是可以避免的。反序列化提供了一个很特别的方法,即一个私有、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。想要杜绝上面现象的发生,那么就可以在单例模式中加入readResolve()方法,代码如下:
private Object readResolve() {
// 此处返回单例模式中的实例对象
return sInstance;
}
--------------------------------------------------分割线-------------------------------------------------------
分割线中内容见:Java设计模式——单例模式(Singleton pattern)
看到上面代码也是耳目一新,看代码解决多次实例化应该是没问题的[我是没有验证过];我感到不解的是一般是JavaBean数据类才会序列化存储,但JavaBean好像很少见到单例的JavaBean[我是菜鸡,哪位大佬帮忙解答一下],于是我在评论中请教大佬,大佬的回复让我瞬间“顿悟”:
有兴趣的小伙伴可以自己run一下;
以上几个单例都存在一个问题就是反射创建多个实例对象,下面就是利用Java枚举enum实现的单例
public enum SingltenEnum { INSTANCE; public void utils() { //utils 具体逻辑 } }
枚举单例:
在需要调用的utils工具是只需要:SingltenEnum.INSTANCE.utils(); 即可调用utils 方法;
枚举单例和静态内部类都是巧妙的利用的JVM机制实现单例,同时枚举类单例更是
借助枚举特征将 反射这个神器也拦截在外,实现了正真的单例;但是这个纯正的单例也有JavaAPI普遍的特征就是有点 臃肿执行效率会有所打折,所以个人基本上是使用更轻便的 静态内部类实现单例的。移动端开发中代码逻辑相对比较容易,个人认为可以在写代码过程中去花苦力避免反射创建实例,比牺牲性能使用沉重的代码更划算;结束语:
看到这里估计早就有人会说你这些都是少了恶汉模式呢,首先饿汉单例其实要比懒汉单例要容易一些,其次我个人认为恶汉模式预加载带来会过早的霸占内存空间,完全可以用懒汉替代的;在移动端的开发中:首要的与用户交互的界面一定要的有非常舒适的体验,内在的功能再丰富逻辑再复杂也不能违背这一原则,毕竟80%用户只是使用20%功能;这就是需要在移动端做大量的优化逻辑代码的工作提升APP的舒适度,在移动端有限的CPU、GPU、RAM的提前下,尽可能的提升代码的执行效率,优化代码逻辑就是根本所在了。所以更建议使用高效静态内部类来实现单例,移动端开发人员就用苦力避免反射更合适;
由于个人水平有限认知不足,如有纰漏、错误望诸位大佬批评指正!