单例模式深入详细探索!!!

单例模式

饿汉式单例模式:

1、普通方式

package SingleMode;
//饿汉式单例
public class HungryMode {
    private HungryMode(){}

    private final static HungryMode hunMode=new HungryMode();

    public static HungryMode getHunMode(){
        return hunMode;
    }
}

2、静态代码块进行实例

package SingleMode;
//饿汉式单例
public class HungryMode {
    private final static HungryMode hunMode;
    static {
        hunMode=new HungryMode();
    }
    private HungryMode(){}
    public static HungryMode getHunMode(){
        return hunMode;
    }
}

问题:消耗内存。如果对象本身就很大,不使用时就创建消耗资源。

比如:

package SingleMode;
//饿汉式单例
public class HungryMode {
    byte[] data=new byte[1024*1024];
    byte[] data1=new byte[1024*1024];
    byte[] data2=new byte[1024*1024];
    byte[] data3=new byte[1024*1024];
    private HungryMode(){}
    private final static HungryMode hunMode=new HungryMode();
    public static HungryMode getHunMode(){
        return hunMode;
    }
}

创建对象时的四个data对象将会消耗大量资源。

问题存在解决问题:懒汉式单例

懒汉式单例模式

原始模式(重重漏洞)

单线程OK

package SingleMode;
// 懒汉式单例
public class LazyMode {
    private LazyMode(){}
    private static LazyMode lazyMode;
    public static LazyMode getLazyMode(){
        if (lazyMode==null)lazyMode=new LazyMode();
        return lazyMode;
    }
}

多线程测试:

package SingleMode;
// 懒汉式单例
public class LazyMode {
    private LazyMode(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazyMode lazyMode;
    public static LazyMode getLazyMode(){
        if (lazyMode==null)lazyMode=new LazyMode();
        return lazyMode;
    }


    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                  LazyMode.getLazyMode();
            }).start();
        }
    }
}

结果:

image-20221026151855081

显然不行。

问题1:多线程不安全,解决方式加锁

方式一:

方法加锁:

package SingleMode;
// 懒汉式单例
public class LazyMode {
    private LazyMode(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazyMode lazyMode;
    
    public static synchronized LazyMode getLazyMode(){
        if (lazyMode==null)lazyMode=new LazyMode();
        return lazyMode;
    }
}

结果:线程安全

分析:每次调用方法时都要为同步付出代价,而我们实际要同步的是lazyman对象。

问题:性能差,抛弃。

方式二:

双重检查机制:

package SingleMode;
// 懒汉式单例
public class LazyMode {
    private LazyMode(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazyMode lazyMode;
    public static synchronized LazyMode getLazyMode(){
        if (lazyMode==null){
            synchronized (LazyMode.class){
                if (lazyMode==null){
                    lazyMode=new LazyMode();
                }
            }
        }
        return lazyMode;
    }
}

结果:线程安全,延迟加载,效率较高。

分析:具体化解决问题,在创建对象之前才进行同步。

问题:极端概率出错,lazyMode=new LazyMode();不是原子性操作。

解析:lazyMode=new LazyMode();过程

1、分配内存空间(地址)。

2、执行构造方法,创建对象。

3、将对象指向内存空间(地址)。

编译器为了优化执行过程,可能会进行指令重排序(将以上三步顺序打乱)。

问题发生原因:

A线程执行:132

B线程进入方法时,A执行到3,未完成构造。

此时B return对象使用,遇到空指针。

解决问题:lazyman对象加volatile关键字。

优化:

package SingleMode;
// 懒汉式单例
public class LazyMode {
    private LazyMode(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static volatile LazyMode lazyMode;
    public static synchronized LazyMode getLazyMode(){
        if (lazyMode==null){
            synchronized (LazyMode.class){
                if (lazyMode==null){
                    lazyMode=new LazyMode();
                }
            }
        }
        return lazyMode;
    }
}
到此线程安全问题解决,优化完毕。

回想一下静态内部类:

第一:JVM在类初始化时有一个过程,为了保证类在多线程下不被多次加载。在类加载的初始化期间,JVM会获取一个锁,这个锁会同步多个线程对同一个类的初始化。

(这不就天然解决了我们所要的单例!!!)

静态内部类实现:
package SingleMode;

public class LazyMode1 {
    private LazyMode1(){
        System.out.println(Thread.currentThread().getName()+" get instance ok ");
    }

    private static class singleInstance{
        private static final LazyMode1 instance=new LazyMode1();
    }

    public static LazyMode1 getInstance(){
        return singleInstance.instance;
    }

// 测试代码:
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                LazyMode1.getInstance();
            }).start();
        }
    }
}

测试结果:

image-20221112163947411

始终只有一个线程成功创建对象。单例成功!!!

总结:

1.静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象。

(静态内部类是一个比较好的解决方式)

回到双重检查机制,但它是真的安全了吗??????

有一个问题出现了,java中的反射机制提供了另外的创建对象方式。

方式:通过单例对象去得到,修改构造器。

测试:

public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    LazyMode lazyMode = LazyMode.getLazyMode();
    //获取构造器
    Constructor<? extends LazyMode> declaredConstructor = lazyMode.getClass().getDeclaredConstructor(null);
    //构造器公开     
    declaredConstructor.setAccessible(true);
    //构造器创建对象     
    LazyMode lazyMode1 = declaredConstructor.newInstance();
    // 比较两个对象
    System.out.println(lazyMode==lazyMode1);
}

结果:

image-20221112170321451

仅仅在单线程下就成功破坏了单例!

以上方式都破坏了单例,又该怎样解决呢???

思路:创建多个对象的肯定会走多次构造器,我们可不可以修改构造器,让构造方法只调用一次,下一次调用时抛出异常?

改进代码:

private LazyMode(){
    if (lazyMode!=null){
        throw new RuntimeException("不可以调用第二次哦!");
    }
}

再次测试:

image-20221112171038469

明显我们成功了!!!

但是这里去检测的前提是调用方法实例化了类中给的单例对象,所有可以通过判断单例对象是否为空来控制构造方法的调用。

思路:

问题出现了,我们一开始就不用类中的getLazyMode方法去获取单例对象,此时lazymode将一直为null;

我们是不是可以一直调用构造器了???

实践:
class test{
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //获取构造器
        Constructor<? extends LazyMode> declaredConstructor = LazyMode.class.getDeclaredConstructor(null);
        //构造器公开
        declaredConstructor.setAccessible(true);
        //构造器创建对象
        LazyMode lazyMode1 = declaredConstructor.newInstance();
        LazyMode lazyMode2 = declaredConstructor.newInstance();
        // 比较两个对象
        System.out.println(lazyMode2==lazyMode1);
    }
}

结果:

image-20221112172743395

可以看到单例又又又被破坏了!!!

新的思路

通过一个类内部变量控制构造函数只能使用一次

实践:
private static boolean lbcsjdbchidh212=false;
private LazyMode(){
    if (!lbcsjdbchidh212){
        lbcsjdbchidh212=true;
    }else {
        throw new RuntimeException("不可以调用第二次哦!");
    }
}

测试结果:

image-20221112174403845

成功阻止了多个对象创建!

新的思路

新的问题来了,反射可以修改字段值,前提获取变量名称。哪怕我们变量名称通过了加密,始终会有解密然后获取字段名称。

实践:
class test{
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //获取到变量
        Field lbcsjdbchidh212 = LazyMode.class.getDeclaredField("lbcsjdbchidh212");
        //修改变量权限为public
        lbcsjdbchidh212.setAccessible(true);
        //获取构造器
        Constructor<? extends LazyMode> declaredConstructor = LazyMode.class.getDeclaredConstructor(null);
        //构造器公开
        declaredConstructor.setAccessible(true);
        //构造器创建对象
        LazyMode lazyMode1 = declaredConstructor.newInstance();
        //修改变量值
        lbcsjdbchidh212.set(lazyMode1,false);
        // 创建第二个变量
        LazyMode lazyMode2 = declaredConstructor.newInstance();
        // 比较两个对象
        System.out.println(lazyMode2==lazyMode1);
    }
}

结果:

image-20221112175227953

很好,我们又破坏了单例

结论:总是相互对抗的,单例总是有办法破解的

观察源码时的问题

此时我们应该仔细去观察源码,对症下药

观察后发现,反射中创建对象的newInstance方法源码有一个问题:

image-20221112175617616

可以看到枚举类是不能使用newInstance方法的

开始测试一下枚举是否能使用newIntance方法
package SingleMode;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum SingleEnum {
    single;
    public SingleEnum getSingle(){
        return single;
    }
}
class Test{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        SingleEnum single = SingleEnum.single;
        Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        SingleEnum single1 = declaredConstructor.newInstance();
        System.out.println(single==single1);
    }
}

结果:image-20221112180831929

明显反射破坏枚举失败,

但是抛出的异常居然是 java.lang.NoSuchMethodException,提示没有这个构造方法。

我们想看到的不是:

image-20221112181216146

可是构造方法不重载不就是默认的无参构造吗?

why???

我们去反编译这个枚举类,查看源码,真的没有无参构造吗?

image-20221112181934217

发现是无参构造的呀!

会不会我们没有看到真正的源码?

我们这里使用了反编译工具jad进行反编译查看源码

image-20221112183126956

完成后我们应该算是看到了源码了

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   SingleEnum.java

package SingleMode;


public final class SingleEnum extends Enum
{

    public static SingleEnum[] values()
    {
        return (SingleEnum[])$VALUES.clone();
    }

    public static SingleEnum valueOf(String s)
    {
        return (SingleEnum)Enum.valueOf(SingleMode/SingleEnum, s);
    }

    private SingleEnum(String s, int i)
    {
        super(s, i);
    }

    public SingleEnum getSingle()
    {
        return single;
    }

    public static final SingleEnum single;
    private static final SingleEnum $VALUES[];

    static 
    {
        single = new SingleEnum("single", 0);
        $VALUES = (new SingleEnum[] {
            single
        });
    }
}

明显发现问题所在,源码里真的没有无参构造!!!!!

构造方法为:

private SingleEnum(String s, int i)
    {
        super(s, i);
    }

接下来我们获取这个构造器,继续尝试反射构造枚举对象

package SingleMode;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum SingleEnum {
    single;
    public SingleEnum getSingle(){
        return single;
    }
}
class Test1{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        SingleEnum single = SingleEnum.single;
        //获取参数为(String,int)的构造器
        Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        SingleEnum single1 = declaredConstructor.newInstance();
        System.out.println(single==single1);
    }
}

结果:

image-20221112184157488

终于是我们想要的结果了

看来IDE欺骗了我们!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱打辅助的小可爱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值