能和面试官吹牛的单例模式

今天小菜鸡我又来了,分享一个单例模式,叫我知识的搬运工

此博客的源泉: https://www.bilibili.com/video/BV1K54y197iS 【小狂神】

一、了解最基本的单例

1.1 概念:

  1. 单例类只能有一个实例
  2. 这个实例由这个单例类内部创建
  3. 这个实例必须提供给外界

1.2 创建方式

【重要】:构造器私有化
个人认为创建方式可以分为两大类:
1.饿汉式:需要时才创建
2.懒汉式:加载的时候就创建

二、思路及代码实现

2.1 饿汉式基本实现

代码:

public class HungerSingle {
	//构造器私有化
	private HungerSingle(){}
	//创建这个类实例
	private HungerSingle single = new HungerSingle();
	//给外界一个接口,提供单例实例
	//注意:这里只能是static方法,因为该类无法在外部new实例化,构造器私有化了
	public static HungerSingle  getSingle(){
		return single;
	}
}

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

为了解决上述问题,可以利用懒汉模式

2.2 懒汉式

我需要用的时候再去加载

public class LazySingle{
	//构造器私有化
	private LazySingle(){}
	//定义
	private static LazySingle single;
	//给外界提供实例
	public static LazySingle getSingle(){
		//没有才创建
		if(single == null){
			single = new LazySingle();
		}
		return single;
	}
}

然而。。。。
事情永远都是这么简单就好了

这样写单例会出现问题,及线程不安全

测试一下

class LazySingle{
    //构造器私有化
    private LazySingle(){
        System.out.println(Thread.currentThread().getName());
    }
    //定义
    private static LazySingle single;
    //给外界提供实例
    public static LazySingle getSingle(){
        //没有才创建
        if(single == null){
            single = new LazySingle();
        }
        return single;
    }
}

public class MyTest{
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazySingle.getSingle();
            },"No."+i).start();
        }
    }
}

结果:获得了多个实例
在这里插入图片描述

线程不安全,这事好办,加锁

2.3 双重检测锁(DCL)

class LazySingle{
    //构造器私有化
    private LazySingle(){
        System.out.println(Thread.currentThread().getName());
    }
    //定义
    private static LazySingle single;
    //给外界提供实例
    public static LazySingle getSingle(){
        //没有才加锁,增加效率
        if(single == null){
            synchronized(LazySingle.class){
                //没有才创建
                if(single == null){
                    single = new LazySingle();
                }
            }
        }
        return single;
    }
}

public class MyTest{
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazySingle.getSingle();
            },"No."+i).start();
        }
    }
}

测试之后,没得问题了

但是我细细的比对大佬的代码后,我发现我少了一个关键字 volatile

如何加?

 //定义
private volatile static LazySingle single;

啥是volatile?有啥作用?这就有得说了?

任意门👉 (volatile 待更新。。。。)

总之:加volatile是为了出现脏读的出现,保证操作的原子性。

2.4 静态内部类实现单例(推荐方式)

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。

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

三、如何破化单例

无聊的操作,没啥卵用哈

【1】如何破坏?

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

public class LazySingle {
    private static LazySingle single;
    private LazySingle(){
    }
    public static LazySinglegetInstance(){
        //第一次判断,没有这个对象才加锁
        if(single == null){
            //哪个需要保护,就锁哪个
            synchronized (LazySingle.class){
                //第二次判断,没有就实例化
                if(single == null){
                    single = new LazySingle();
                }
            }
        }
        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
    }
}

步骤:

  1. 通过反射得到字节码对象
  2. 设置setAccessible(true)
  3. 通过构造器对象的newInstance();

【2】如何防止被破坏?
正所谓道高一尺魔高一丈,来battle,battle!

既然这次你是通过得到构造器破化的,那我给构造器加个方法,如果你已经创建了实例,那就抛出异常

public class LazySingle{
    private static LazySingle single;
    //给构造器加个方法
    private Single(){
    	 synchronized(LazySingle.class){
	        if(single!=null){
	            throw new RuntimeException("破坏失败");
	        }
		}
    }
    public static LazySingle getInstance(){
        //第一次判断,没有这个对象才加锁
        if(single == null){
            //哪个需要保护,就锁哪个
            synchronized (LazySingle.class){
                //第二次判断,没有就实例化
                if(single == null){
                    single = new LazySingle();
                }
            }
        }
        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
    }
}

但是这个又有问题,这里的判断是private static SingleSingle 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
}

再实例化之前,我把标志改回来,又不能绝对单例了,emmmm。。。。

四、枚举实现绝对单例(拓展)

利用枚举实现不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化(菜鸟教程官方术语)

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

这里又涉及到枚举了。。。。。

通过反射不能破化枚举
原因:枚举的构造方法不能为public ,语言特性。。。

详情了解。。。
任意门👉 (枚举,待更新。。。。。)


吐了吐了。。。。就到这里了,挖的坑有时间再填,写博客这玩意太耗时间了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值