深入理解单例模型

深入单例模式

单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到喝多东西,比如 多线程是否安全,是否懒加载,性能等等。还有你知道几种单例模式的写法?如何防止反射或坏单例模式?今天 我们来探究单例模式。

1、饿汉式

public class Hungey {
    private Hungey() {

    }
    private final static Hungey hungry = new Hungey();
    public static Hungey getInstance() {
        return hungry;
    }
}

饿汉式是最简单的单例模式的写法,保证了线程的安全,在很长时间里,都是饿汉式来完成单例的,因为够简单,其实饿汉式有一点小问题,看下面代码:

public class Hungey {
    private byte[] data1 = new byte[1024];
    private byte[] data2 = new byte[1024];
    private byte[] data3 = new byte[1024];
    private byte[] data4 = new byte[1024];
    private Hungey() {

    }
    private final static Hungey hungry = new Hungey();
    public static Hungey getInstance() {
        return hungry;
    }
}

在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化了,并且放入内存了,如果长时间没有用到getInstance方法,不需要Hungry的对象,这不是一种浪费吗?我希望的是 只有用到了getInstance方法,才会去初始化单例类,才会去加载单例类中的数据。所以就有了第二种单例模式:懒汉式。

2、懒汉式

正常 懒汉式单例:

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"Start");
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
	//测试并发环境,发现单例失败
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}

多一层检验可以避免问题,也就是DCL懒汉式

public class LazyMan {
    private LazyMan(){
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

DCL懒汉式的单例,保证了线程的安全性,又符合懒加载,只有在用到的时候,才会去初始化,调用效率也比较高,但是这种写法在极端情况下还是可能有一定的问题。因为

lazyMan = new LazyMan();

不是原子操作,至少经过三个步骤:

  1. 分配对象内存空间
  2. 执行构造方法初始化对象
  3. 设置instance执行刚分配的内存地址。

由于指令重排,导致A线程执行lazyMan=new LazyMan();的时候,可能会先执行了第三步(还没执行第二步),此时线程B又进来了发现lazyMan液晶部位空了,直接返回lazyMan,并且后面使用了返回的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错误,所以就有了下面的一种单例模式。

这种单例模式只是在上面DCL单例模式增加了一个volatile关键字来避免指令重排

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"Start");
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

3、静态内部类

还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接出示哈,不过是移到了静态内部类,十分巧妙。既保证了线程安全型,同时又满足了懒加载。

public class Holder {
    private Holder() {
        
    }
    public static Holder getInstance() {
        return InnerClass.holder;
    }
    private static class InnerClass{
        private static final Holder holder = new Holder();
    }
}

4、万恶的反射

万恶的反射登场了,反射是一个比较霸道的东西,无视private修饰的构造方法,可以直接在外面newInstance,破坏我们辛辛苦苦写的单例模式。

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    LazyMan lazyMan = LazyMan.getInstance();
    Constructor<LazyMan> declaredConstrutor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstrutor.setAccessible(true);
    LazyMan lazyMan1 = declaredConstrutor.newInstance();
    System.out.println(lazyMan.hashCode());
    System.out.println(lazyMan1.hashCode());
    System.out.println(lazyMan == lazyMan1);
}

我们分别打印出lazyMan,lazyMan1的hashcode,lazyMan1是否相等lazyMan,结果显而易见,不相等;那么,怎么解决这种问题呢?

public class LazyMan {
    private LazyMan(){
        synchronized (LazyMan.class) {
            if (lazyMan != null) {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用getInstance方法,是不会出现这种事情的。

但是这种写法还是有问题:

上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败了。但是如果破坏者干脆不先调用getInstance方法,一上来就直接用反射创建对象,我们的判断就不生效了:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Constructor<LazyMan> declaredConstrutor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstrutor.setAccessible(true);
    LazyMan lazyMan = declaredConstrutor.newInstance();
    LazyMan lazyMan1 = declaredConstrutor.newInstance();
    System.out.println(lazyMan.hashCode());
    System.out.println(lazyMan1.hashCode());
    System.out.println(lazyMan == lazyMan1);
}

那么如何防止这种反射破坏呢?

public class LazyMan {
    private static boolean flag = false;
    private LazyMan(){
        synchronized (LazyMan.class) {
            if (!flag) {
                flag = true;
            }else {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

在这里,我们定义一个boolean变量flag,初始值是false,私有构造函数里面做了一个判断,如果flag=false,就把flag改为true,但是如果flag等于true,就说明有问题了,因为正常调用时不会第二次跑到私有构造方法的,所以抛出异常。

看起来很美好,但是还是不能完全防止反射破坏单例模式,因为可以利用反射修改flag的值。

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    Constructor<LazyMan> declaredConstrutor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstrutor.setAccessible(true);
    LazyMan lazyMan = declaredConstrutor.newInstance();
    //获取lazyMan flag修改
    Field field = LazyMan.class.getDeclaredField("flag");
    field.setAccessible(true);
    field.set(lazyMan, false);
    LazyMan lazyMan1 = declaredConstrutor.newInstance();
    System.out.println(lazyMan.hashCode());
    System.out.println(lazyMan1.hashCode());
    System.out.println(lazyMan == lazyMan1);
}

并没有一个很好地方案去避免反射破坏单例模式,所以论到我们的枚举登场了。

5、枚举

枚举类型是java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是一位它既是一种类(class)类型却又比类型多了些特殊的约束,但是这种约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

public enum EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance() {
        return INSTANCE;
    }
}
class Demo01 {
    public static void main(String[] args) {
        EnumSingleton singleton = EnumSingleton.INSTANCE;
        EnumSingleton singleton2 = EnumSingleton.INSTANCE;
        System.out.println(singleton == singleton2);
    }
}

枚举是目前最推荐单例模式的写法,因为足够简单,不需要自己开发保证线程安全,同时又可以有效地防止反射破坏我们的单例模式,我们可以看下newInstance的源码:

image-20210409204957012

重点是红框中圈出来的部分,如果枚举去newInstance就直接抛出异常了。

反编译查看下枚举的源码

javap -p EnumSingleton.class

Compiled from "EnumSingleton.java"
public final class singleton.EnumSingleton extends java.lang.Enum<singleton.EnumSingleton> {
  public static final singleton.EnumSingleton INSTANCE;
  private static final singleton.EnumSingleton[] $VALUES;
  public static singleton.EnumSingleton[] values();
  public static singleton.EnumSingleton valueOf(java.lang.String);
  private singleton.EnumSingleton();
  public singleton.EnumSingleton getInstance();
  static {};
}

于是我们再次尝试使用反射空参构造生成枚举。

class Demo01 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumSingleton enumSingleton = constructor.newInstance();
    }
}

出现了预期之外的错误Exception in thread "main" java.lang.NoSuchMethodException: singleton.EnumSingleton.<init>()意为创建这个空参数构造器失败。

使用jad工具进行反编译:

jad -sjava EnumSingleton.class

点开这里的反编译java文件

public final class EnumSingleton extends Enum
{

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

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(singleton/EnumSingleton, name);
    }

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

    public EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

发现没有空参构造函数,只有一个(String,int)构造函数,我们改变反射再次尝试用反射创建枚举。

class Demo01 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton enumSingleton = constructor.newInstance();
    }
}

确实无法破坏,出现了预期的错误Cannot reflectively create enum objects

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值