深刻探讨单例模式

最近学习了一下单例模式,看bilibili up主“狂神说Java”讲完后,发现大部分博客都少了一个颇有趣的环节,不分享出来实在是太惋惜了,原视频 https://www.bilibili.com/video/BV1K54y197iShtml

一、了解单例

这个部分小部分我相信不少博客都讲的很好,我就尽可能精简了
  1. 注意:
  • 单例类只能有一个实例
  • 这个实例由本身建立
  • 这个实例必须提供给外界
  1. 关键:构造器私有化
  2. 建立方法:
  • 饿汉式
  • 懒汉式

总结:我认为建立方法能够归根于两种,一种是饿汉式,我在类的加载的时候就建立;还有一种懒汉式,只有在我须要的时候才去建立

java

二、思路及实现

【饿汉模式最基本的实现】面试

在类加载的时候就已经建立了,这个模式下,线程是安全的,不一样的线程拿到的都是同一个实例,可是,这个也存在空间浪费的问题,我不须要的时候你也加载了。安全

//饿汉模式
 public class HungerSingle {
    private static HungerSingle single = new HungerSingle();
    //构造器私有,外界不能经过构造方法new对象,保证惟一
    private HungerSingle() {
    }
    //提供外界得到该单例的方法,注意方法只能是static方法,由于没有类实例
    public static HungerSingle getInstance(){
        return single;
    }
}

【懒汉模式最基本的实现】多线程

为了解决上述那个空间浪费问题,这时候懒汉模式就起做用了,你须要个人时候我再去建立这个实例ide

//懒汉模式
public class LazySingle {
    private static LazySingle single;
    //构造器私有化,禁止外部new生成对象
    private LazySingle(){
    }
    //外界得到该单例的方法
    public static LazySingle getInstance(){
        if(single == null){
            single = new LazySingle();
        }
        return single;
    }
 }

一位热心前辈的评论:“像你这样写单例,在咱们公司是要被开除的。”
趁我仍是学生,怀着之后不被开除的心情,继续学习下去
原来懒汉模式下,单例线程是不安全的。工具

怎么测试呢?以下学习

【测试懒汉模式线程不安全】测试

//一、构造器
private LazySingle(){
    System.out.println(Thread.currentThread().getName());
}

//建立十个线程
for (int i = 0; i < 10; i++) {
    new Thread(()->{
         Singleton2.getInstance();
    }).start();
}

此时你会发现,构造方法调用了不止一次,说明没有实现预期的单例线程

平时咱们解决线程不安全的方法:不就是线程不安全嘛,那好办,加锁

【双重检测锁/DCL】

public class DCLSingle {
    private static DCLSingle single;
    private DCLSingle(){
    }
    public static DCLSingle getInstance(){
        //第一次判断,没有这个对象才加锁
        if(single == null){
            //哪一个须要保护,就锁哪一个
            synchronized (DCLSingle.class){
                //第二次判断,没有就实例化
                if(single == null){
                    single = new DCLSingle();
                }
            }
        }
        return single;
    }

}

仔细和别人代码一比对,发现我少了个volatile关键字,这是啥玩意?
不懂就问。

【volatile】
为了不指令重排

//上述代码声明上面加上volatile关键字
 private volatile static DCLSingle single;

啥是volatile ?

引用自别人博客
http://www.javashuo.com/article/p-bfigxqiv-bc.html

加volatile是为了出现脏读的出现,保证操做的原子性

一、原子性操做:不可再分割的操做
例如:single = new DCLSingle();
其实就是两步操做:
①new DCLSingle();//开辟堆内存
②singl指向对内存

二、脏读
Java内存模型规定全部的变量都是存在主存当中,每一个线程都有本身的工做内存。
线程对变量的全部操做都必须在工做内存中进行,而不能直接对主存进行操做。
而且每一个线程不能访问其余线程的工做内存。
变量的值什么时候从线程的工做内存写回主存,没法肯定。

三、指令重排
single = new DCLSingle();
先执行②
后执行①
//先指向堆内存,还未完成构造


【模拟状况】
①线程1执行,在本身的工做内存定义引用,先指向堆内存,还未构造完成
②此时线程2执行,它进行判断,引用已经指向了内存,因此线程2,认为构造完成,实际还未构造完成

还有一种差点忘记说了,也是菜鸟教程说建议使用的方式

【静态内部类实现单例】

public class Singleton {
    private Singleton(){}
    private static class SingleIN{
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton getInstance(){
        return SingleIN.INSTANCE;
    }
}

你会发现它和前面讲的普通饿汉式很像,我把它也归于饿汉式一类,由于它也是直接就new Singleton,可是它却有着懒加载的效果,而这种方式是 Singleton 类被装载了,instance 不必定被初始化。由于 SingletonHolder 类没有被主动使用,只有经过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。

【建议】建议使用静态内部类实现


## 三、如何破化单例(其它大部分博客没有的内容) 在这里感谢b站up【狂神说java】

在面试官面前装逼的时候来了

java语言实现动态化的灵魂——反射,说:没有什么是我不能改变的,看我来如何操做。

【反射破坏单例】

public class DCLSingle {
    private static DCLSingle single;
    private DCLSingle(){
    }
    public static DCLSingle getInstance(){
        //第一次判断,没有这个对象才加锁
        if(single == null){
            //哪一个须要保护,就锁哪一个
            synchronized (DCLSingle.class){
                //第二次判断,没有就实例化
                if(single == null){
                    single = new DCLSingle();
                }
            }
        }
        return single;
    }
    
    //经过反射破化单例
    public static void main(String[] args) throws Exception {
        LazySingle single = LazySingle.getInstance();
        Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingle single1 = constructor.newInstance();
        System.out.println(single == single1);//false
    }

}

获得单例类的构造器,而后经过newInstance的方法建立对象,很明显破化了单例

【改进代码,防止你搞破化】

既然此次你是经过获得构造器破化的,那我给构造器加个方法,若是你已经建立了实例,那就抛出异常

private LazySingle(){
    synchronized(LazySingle.class){
        if(single!=null){
            throw new RuntimeException("破坏失败");
        }
    }
}

可是这个又有问题,这里的判断是private static DCLSingle single 是否有值,若是咱们都不经过getInstance()方法建立对象,而是这样

public static void main(String[] args) throws Exception {
 //   LazySingle single = LazySingle.getInstance();
    Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    
    //注意:这里的对象不是单例类中里面属性的那个对象
    LazySingle single = constructor.newInstance();
    LazySingle single1 = constructor.newInstance();
    System.out.println(single == single1);//false
}

这里根本不会抛出异常,而是又破坏了单例

【继续改进代码,防止搞破化】
简直就是相爱相杀呀,咱们能够利用红路灯原理,防止破化
改进构造方法

//加个标志
private static String sign = "password";
private LazySingle(){
    synchronized(LazySingle.class){
        if(single!=null || !"password".equals(sign)){
            throw new RuntimeException("破坏失败");
        }else{
            sign = "no";
        }
    }
    
}

此刻你经过上述main()方法里面的内容测试,发现又会抛出异常。然而咱们能经过反射得到构造方法,那咱们一样也能经过反射获取对象的属性以及值吧

【再度破化】

public static void main(String[] args) throws Exception {
    Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Field field = LazySingle.class.getDeclaredField("sign");
    //此处省略经过反射获取该属性的类型和方法....
    LazySingle single1 = constructor.newInstance();
    //从新变回原标志位
    field.set("sign","password");
    LazySingle single2 = constructor.newInstance();
    System.out.println(single2 == single1);//false
}

又被破化了

【再次改进】

咱们将目光抛向枚举,
jdk1.5以后,出现枚举
利用枚举实现不只能避免多线程同步问题,并且还自动支持序列化机制,防止反序列化从新建立新的对象,绝对防止屡次实例化(菜鸟教程官方术语)

public enum Singleton {  
    INSTANCE;  
    public Singleton getInstance() {  
        return INSTANCE
    }  
}

【反射能破化枚举的单例吗?】

  1. 咱们先要了解枚举是啥,它的底层是怎么实现的
  2. 咱们会发现枚举自己就是一个
  3. 经过反编译工具,查看枚举底层的构造方法
  4. 经过反射获取构造方
  5. 重复上述测试

咱们最终能够发现反射不能破化枚举的单例

这种实现方式尚未被普遍采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止屡次实例化。(菜鸟教程官方)

【总结】太难了