单例模式常用写法与弊端

所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。在Java,一般常用在工具类的实现或创建对象需要消耗资源。

特点:

  • 类构造器私有
  • 持有自己类型的属性
  • 对外提供获取实例的静态方法

单例模式写法共分为以下几类

  • 1、饿汉式
  • 2、懒汉式
  • 3、静态内部类
  • 4、枚举类

1、饿汉式

顾名思义,饿汉,我很饿,需要一开始就创建,实例代码如下:

/**
 * 饿汉单例模式
 * 缺点:类初始化就加载信息,类中可能存在比较大的对象,浪费性能
 *
 * @author lixiang
 * @version V1.0
 * @date 2020/3/16 11:24
 **/
public class Hungry {


    private byte[] data1 = new  byte[10240];
    private byte[] data2 = new  byte[10240];
    private byte[] data3 = new  byte[10240];
    private byte[] data4 = new  byte[10240];


    /**
     * 类初始化直接创建对象
     */
    private static Hungry hungry = new Hungry();

    /**
     * 私有构造方法
     */
    private Hungry() {

    }

    /**
     * 共有获取实例对象方法
     *
     * @return 初始化的实例对象
     */
    public static Hungry getInstance() {
        return hungry;
    }
}

缺点也很明显,当类被加载时就直接实例化类,如果此类中存在一些比较大的对象,这些对象就会直接被加载浪费性能。

2、懒汉式

顾名思义,我很懒,需要我的时候我在创建,

/**
 * 懒汉单例模式
 * 缺点  多线程下 对象并不是单例的
 *
 * @author lixiang
 * @version V1.0
 * @date 2020/3/16 11:33
 **/
public class LazyMan {
    private static LazyMan lazyMan;

    private LazyMan() {

    }

    private static LazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + LazyMan.getInstance());
            }, String.valueOf(i)).start();
        }
    }
}

缺点,并发情况下,对象无法保证单例,违背初衷!(执行代码中的main方法即可破解)

2.1 懒汉式(DCL)

由于上一版本懒汉模式在多线程情况下无法保证单例。因此采用锁进行处理。但是只使用synchronized会出现部分构造的现象,加入volatile也可以避免指令重排。实例代码如下:

/**
 * 懒汉单例模式 DCL(双重校验锁)
 * 反射可以破坏单例
 *
 * @author lixiang
 * @version V1.0
 * @date 2020/3/16 11:33
 **/
public class LazyMan2 {
    private volatile static LazyMan2 lazyMan;

    private LazyMan2() {

    }

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

    public static void main(String[] args) {
        try {
            invokeClass();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void invokeClass() throws Exception {
        Constructor<LazyMan2> declaredConstructor = LazyMan2.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan2 lazyMan2 = declaredConstructor.newInstance();
        LazyMan2 lazyMan21 = declaredConstructor.newInstance();

        System.out.println(lazyMan2);
        System.out.println(lazyMan21);
    }
}

你认为这样就可以了么?如果是按照正常思路,这个获取的对象肯定是满足要求的,但是如果使用反射就无法保证反射的对象单例了(详细见main方法)

3、静态内部类

利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

/**
 * 静态内部类
 *
 * @author lixiang
 * @version V1.0
 * @date 2020/3/16 13:39
 **/
public class Holder {
    private Holder(){};

    private static Holder getInstance(){
        return InnerClass.holder;
    }

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

4、枚举类

利用枚举对象每个实体都是单例的原理,引发出来了最简单的单例模式。

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

可能会有人疑问,枚举类可以利用反射构造方法进行创建么?可以看一下Constructor.newInstance(Object … initargs)源码,截图如下:
在这里插入图片描述
我们发现,Constructor.newInstance方法源码中class做了判断,如果是枚举类型就会抛出Cannot reflectively create enum objects异常。

作为一个开发人员,不自己尝试一下怎么能够让你信服。

尝试利用反射破解枚举类的单例

下面我们利用反射尝试操作一下,使其出现Cannot reflectively create enum objects异常。

/**
 * 枚举
 *
 * @author lixiang
 * @version V1.0
 * @date 2020/3/16 13:47
 **/
public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        EnumSingle enumSingle2 = EnumSingle.INSTANCE;

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        // 期望的异常 throw new IllegalArgumentException("Cannot reflectively create enum objects");

        //  java.lang.NoSuchMethodException: com.coding.single.EnumSingle.<init>()
        declaredConstructor.newInstance();
    }
}

执行后发现运行结果是NoSuchMethodException,遇到这里发现我们实际出现的异常与预期的异常不符合,我们需要进行深入分析,实际异常信息如下:

Exception in thread "main" java.lang.NoSuchMethodException: com.thislx.juc.single.EnumSingle.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.thislx.juc.single.EnumSingle.main(EnumSingle.java:23)

这时候我们要考虑是不是枚举类没有默认无参构造方法呀,我们使用idea开发工具进行查看。发现存在无参构造方法。如下图所示:
在这里插入图片描述
这是什么情况?我们在使用javap命令进行查看,发现依旧存在无参构造方法。如下图所示:
在这里插入图片描述
中途放弃不是一个合格的开发人员,我们使用第三方编辑工具jad进行class的反编译。
在这里插入图片描述
使用如下命令:

jad -sjava EnumSingle.class

在这里插入图片描述
打开EnumSingle.java,看到源代码的我们应该一目了然,原来枚举类只有一个
private EnumSingle(String s, int i) 有参构造方法。
代码如下:

// 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:   EnumSingle.java

package com.thislx.juc.single;

import java.lang.reflect.Constructor;

public final class EnumSingle extends Enum
{

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

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/thislx/juc/single/EnumSingle, name);
    }

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

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static void main(String args[])
        throws Exception
    {
        EnumSingle enumSingle2 = INSTANCE;
        Constructor declaredConstructor = com/thislx/juc/single/EnumSingle.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        declaredConstructor.newInstance(new Object[0]);
    }

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

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

到此我们再次修改我们尝试反射枚举的main方法,修改点:在获取构造方法时传入String.classint.class。修改后代码如下:

public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        // 期望的异常 throw new IllegalArgumentException("Cannot reflectively create enum objects");
        declaredConstructor.newInstance();
    }
}

运行项目,发现终于出现了Cannot reflectively create enum objects,错误信息,这足以证明枚举实现单例模式是最安全的,无法被多线程、反射破坏
在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值