太长不看版
DCL、Holder(静态内部类)和饿汉式单例都可以通过反射或者序列化破坏。
直接用枚举单例就完事了。
为什么要用单例模式
场景:多线程情况下操作相同对象应该是同一个对象。例如文件。
解决办法
在实例化过程中,所有的对象只实例化一次。
需要实现一个实例化的过程并且向用户提供一个返回实例对象的方法。
分析角度
- 线程安全性。
- 性能。
- 懒加载。
单例模式的分类
饿汉式
在加载时就产生实例化对象存放在堆中,后续使用时只会取到同一个实例化对象,故判断其线程安全。(Ref:JVM ClassLoader)。
不存在延迟加载的情况,长时间不使用(或者说首次使用时间较迟)仍然会保存在堆内存中,若单例数据较大的时候会产生内存浪费,甚至可能会产生内存溢出影响性能。
因此比较适合少量数据的单例模式。public class HungrySingleton { private byte[] data=new byte[1024]; private static HungrySingleton instatance=new HungrySingleton(); private HungrySingleton(){ } public static HungrySingleton getInstance(){ return instatance; } }
懒汉式
为了克服单例对象数据较大且还未被使用时造成的内存浪费,将饿汉式做出一定改变,在首次使用时才进行加载。
懒汉式并非线程安全的:假设同时执行了两个线程,instance==null判定成立的话就会产生两个实例,尽管解决了一部分性能问题,但是却带来了线程不安全的问题。public class HoonSingleton { private byte[] data=new byte[1024]; public static HoonSingleton instance=null; private HoonSingleton(){ } public HoonSingleton getInstance(){ if(instance==null) instance=new HoonSingleton(); return instance; } }
此时,对代码做一定改造以验证线程不安全性。
public class HoonSingleton { private byte[] data=new byte[1024]; public static HoonSingleton instance=null; private HoonSingleton(){ } public static HoonSingleton getInstance(){ if(instance==null) { //为了验证线程不安全而增加的代码块 try { Thread.sleep(100L); } catch (InterruptedException e) { e.printStackTrace(); } instance=new HoonSingleton(); } return instance; } }
验证方法如下:
private static void checkHoonThreadSafe(){ for (int i = 0; i < 20; i++) { new Thread(()-> { System.out.println(HoonSingleton.getInstance()); }).start(); } }
可以看到输出中每一个生成的实例都不一样嗷~
好,既然看到了问题,再找解决方案:
在getInstance方法上增加同步关键词synchronized
执行验证方法,可以看到获取到的实例都是同一个啦。
但是这样又带来了新的问题,加上synchronized方法的时候,getInstance方法又从并行执行退化到了串行执行,带来了性能问题……
(打扰了一下子没能想到怎么去写验证方法所以只能先这么口嗨两句啦)
在getInstance方法中实例化的代码块上加上synchronized。
如此一来,懒汉式单例就变成了如下形式:
public class HoonSingleton { private byte[] data=new byte[1024]; public static HoonSingleton instance=null; private HoonSingleton(){ } public static synchronized HoonSingleton getInstance(){ if(instance==null) { synchronized (HoonSingleton.class){ try { Thread.sleep(100L);//为了验证线程不安全而增加的代码块 } catch (InterruptedException e) { e.printStackTrace(); } instance=new HoonSingleton(); } } return instance; } }
相较于解决方案1,锁的级别从方法变成了更小范围的一个代码块。在性能上会有所提高……
看着视频写完不大对(其实是我并没有看完……),感觉没什么蛇皮用呀……
假定线程AB同时执行,同时判断instance==null,A获取到了锁,B没有获取到锁需要等待。此时A完成实例化释放锁,B在A释放后获取到锁,那么就又进入了实例化过程。
(抱歉这里的验证我也没想出来怎么做……后续万一上网去抄抄看)DCL(Double Checked Locking)
如何去解决2中B在A释放了锁之后实例化的问题呢?
双保险:再去判断一次,也就是双重判断锁DCL
只需要在2的基础上再加上一次判断就行啦
public class DCL { private byte[] data=new byte[1024]; public static DCL instance=null; private DCL(){ } public static synchronized DCL getInstance(){ if(instance==null) { synchronized (DCL.class){ try { Thread.sleep(1000L);//为了验证线程不安全而增加的代码块 } catch (InterruptedException e) { e.printStackTrace(); } if(instance==null)//在获取到了锁之后再进一步判断是否存在实例 instance=new DCL(); } } return instance; } }
但是这是个完美的解决方案么?
如果在该单例实例化对象时,实例化其成员对象时可能会出现指令重排导致空指针异常。
instance=new Singleton();//这行代码在执行时会被拆分成如下三步 memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址 //在实际执行过程中,可能会被重排成 memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化 ctorInstance(memory); //2:初始化对象
如果被重排成上述形式,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得instance对象进行使用就可能发生错误。
解决方案:给instance加上volatile。
所以哪怕是DCL也没能摆脱使用synchronized,仍然会影响一部分性能。
Holder模式
声明类的时候,成员变量中不申明实例变量,而放到一个内部静态类中去申明。
public class Singleton { /** * 带有Holder的方式 * 类级内部类,也就是静态的成员内部类,该内部类的实例与外部类的实例没有绑定关系 * 只有被调用的时候才会装载,从而实现了延迟加载,即懒汉式 */ private Singleton() { } private static class SingletonHolder { /** * 静态初始化器,由JVM来保证线程安全 */ public static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
枚举实现(推荐)
public enum EnumSingleton { INSTANCE; }
这也太TM简单粗暴了……
从安全角度出发说明推荐枚举单例的原因
DCL可以通过反射攻击破坏嗷~
对于实现了序列化接口的单例还可以通过序列化攻击进行破坏嗷~
最后放一个好心人的链接,文末还介绍了单例的破坏以及防御~