单例模式还能这样 ——致敬狂神

饥汉模式(积极加载)

public class Hungry {
	byte[] byts1 = new byte[1024*1024];
	byte[] byts2 = new byte[1024*1024];
	byte[] byts3 = new byte[1024*1024];
	byte[] byts4 = new byte[1024*1024];

	private Hungry(){}//构造器私有化
	private static Hungry instance = new Hungry();//加载类模板时,已经创建实例
	public Hungry getInstance() {//公有化get方法,提供实例
		return instance;
	}
}

从上面实例,我们可以看出饥汉模式的弊端就是,当这个类会占用相当的内存时, 未被使用,就已经实例化,会造成相当的内存浪费! 于是有了 懒汉模式。

懒汉模式(懒加载,使用时才加载)

public clacc Lazy {
	private Lazy(){}
	private static Lazy instance;
	public Lazy getInstance(){
		instance = new Lazy();//方法被执行时才创建实例
		return instance;
	}
}

//以上的写法真的是单例吗?  单线程下是ok的,  但是我们看看多线程情况下
class Test{
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()-> Lazy.getInstance()).start();
        }
    }
}

显而易见的,创建了多个实例,违背了单例!
针对以上情况,我们可以加锁,来保证多线程情况下也是单例的安全

public class Lazy {
    private Lazy(){
        System.out.println(Thread.currentThread().getName() + "已被创建!");
    }
    private static Lazy instance;

    public static Lazy getInstance(){
        if(instance == null) {
        	//这个对象还未被创建,我们不能锁null,所以给它的类模板对象上锁。
        	//new对象是根据类模板来new的
            synchronized (Lazy.class) {
                if (instance == null) {
                    instance = new Lazy();
                }
            }
        }
        return instance;
    }
}

这样似乎是完善了,不,并没有 看如下

public class Lazy {
    private Lazy(){
        System.out.println(Thread.currentThread().getName() + "已被创建!");
    }
    
    //所以我们要加volatile关键字告诉jvm它是易变的 不要优化策略而进行指令重排
    private static volatile Lazy instance;

    public static Lazy getInstance(){
        if(instance == null) {
            synchronized (Lazy.class) {
                if (instance == null) {
                    instance = new Lazy();//这不是一个原子型操作
                   /**
                     * 1.分配内存空间
                     * 2.执行构造方法
                     * 3.引用变量指向内存空间
                     *     虚拟机jvm执行时,可能会产生指令重排的现象 类似 132 的顺序
                     *     这将导致并发场景下的另一线程想要获取实例时,
                     *     锁之前的判空就会认为不为空了
                     *     则会返回 指向未知内存的引用(因为实际上未执行构造方法)
                     */
                }
            }
        }
        return instance;
    }
}

以上,就是较为完善的 DCL懒汉式单例。

静态内部类的 单例实现

public class Holder {

    private Holder(){}

    public static class Innerc {
        private static final Holder holder = new Holder();
    }

    public static Holder getInstance() {
        return Innerc.holder;
    }
}

静态内部类实现单例有哪些好处呢?
静态内部类不会在外部类被加载时就加载,所以也具备懒汉的特质。
另外jvm在多个线程同时调用同一个类模板的cInit方法时,总会有某个线程最先执行,那么其它线程会进入阻塞态。那个真正执行cInit方法的线程完成执行后,其它线程将不再会去执行这个已经完成了模板初始化的类的cInit方法。所以保证了 多线程情况下是安全的。
听起来似乎很厉害? 但在得到这个单例的同时,确实不太好传入外部参数。
up主觉得,可以在得到单例后强行设置嘛~~~

public class Holder {

    private String arg;

    @Override
    public String toString() {
        return "Holder{" +
                "arg='" + arg + '\'' +
                '}';
    }

    public void setArg(String arg) {
        this.arg = arg;
    }

    private Holder(){}

    public static class Innerc {
        private static final Holder holder = new Holder();
    }

    public static Holder getInstance() {
        return Innerc.holder;
    }
}

class Test0 {
    public static void main(String[] args) {
        Holder instance = Holder.getInstance();
        instance.setArg("哈哈");
        System.out.println(instance);
    }
}

然而事情依然没有结束!!!

为什么需要枚举的单例实现

我们知道Java中存在一项技术,叫做反射!!
它可以改变类的构成元素,包括访问修饰符的限制!! 例如

import java.lang.reflect.Constructor;

public class ReflectBrokeSingle {
    public static void main(String[] args) throws Exception {
        //首先我们获取一个单例对象
        Lazy lazy1 = Lazy.getInstance();

        //利用反射,得到它的构造器
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//修改构造器的对外访问权限
        Lazy lazy2 = declaredConstructor.newInstance(null);

        System.out.println(lazy1.hashCode());
        System.out.println(lazy2.hashCode());
    }
}

我们发现它又创建了2个不同的实例!!!
然后一个颇具针对性的解决方案,解决这种反射式破坏

public class Lazy {
    private Lazy(){
        synchronized (Lazy.class) {
            if (instance != null) {
                throw new RuntimeException("不要试图通过反射破坏单例!");
            }
        }
    }
    
    //所以我们要加volatile关键字告诉jvm它是易变的 不要优化策略  进行指令重排
    private static volatile Lazy instance;

    public static Lazy getInstance(){
        if(instance == null) {
            synchronized (Lazy.class) {
                if (instance == null) {
                    instance = new Lazy();
            }
        }
        return instance;
    }
}

上面那种破坏被针对了,但完全无济于事。因为构造方法中判断的实例,是通过静态方法getInstance赋值的。 所以我们依然可以极其轻松的用反射破坏它!

public class ReflectBrokeSingle {
    public static void main(String[] args) throws Exception {
        //首先我们获取一个单例对象
//        Lazy lazy1 = Lazy.getInstance();

        //利用反射,得到它的构造器
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//修改构造器的对外访问权限
        Lazy lazy2 = declaredConstructor.newInstance();
        Lazy lazy3 = declaredConstructor.newInstance();

        System.out.println(lazy3.hashCode());
        System.out.println(lazy2.hashCode());
    }
}

不去执行的他自己的getInstance()方法,这样它的判断条件永远不成立。直接通过反射可以一直创建不同的实例!!

死循环了,特么的,怎么解决,总能想法子破坏, 反射毒瘤!

心病还须心药医,解铃还须系铃人
我们看下反射创建实例方法的源码
在这里插入图片描述
原来,java是提供了这个保护机制,那就是 通过枚举来阻止反射去 创建实例!
所以我们需要枚举的单例!
这里只给出骨架,实际应用场景,自行丰富

public enum EnumSingle {
    instance;
    public EnumSingle getInstance() {
        return instance;
    }
}

这样终究是,把单例模式画上了比较圆满的句号。 单例:我真的不容易!

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值