【设计模式】单例模式_从入门到手撕源码

本文深入探讨了Java中的单例模式,包括懒汉式、线程安全的懒汉式、饿汉式、双检锁/双重校验锁、登记式/静态内部类和枚举实现方式。每种方式都提供了代码示例和详细解释,同时通过反射分析了单例模式的安全性和如何防止破坏单例。最后,强调了枚举实现单例的可靠性。
摘要由CSDN通过智能技术生成

引言

单例模式,从入门到精通。
单例的6总实现方式!
为什么枚举是最靠谱的单例模式?
从源码一一详细剖析!

简介

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 1、单例类只能有一个实例
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例

一、懒汉式

1.代码

public class LazyMan {
    private static LazyMan instance = null;

    private LazyMan() {
    }

    public static LazyMan getInstance(){
        if(instance==null){
            instance = new LazyMan();
            return instance;
        }
        return instance;
    }
	//测试
    public static void main(String[] args) {
        LazyMan instance1 = LazyMan.getInstance();
        LazyMan instance2 = LazyMan.getInstance();
        System.out.println(instance1==instance2);//true
    }
}

2.说明

这种方式简单易懂,有助于新手理解,但严格意义上讲并不是单例模式,因为线程不安全

二、懒汉式(线程安全)

public class LazyManThreadSecurity {
    private static LazyManThreadSecurity instance = null;

    private LazyManThreadSecurity() {
    }

    public static LazyManThreadSecurity getInstance(){
        if(instance==null){
            //加锁,只能用类的锁才能保证一个类只有一个实例
            synchronized (LazyManThreadSecurity.class){
                instance = new LazyManThreadSecurity();
                return instance;
            }
        }
        return instance;
    }
	//测试
    public static void main(String[] args) {
        LazyManThreadSecurity instance1 = LazyManThreadSecurity.getInstance();
        LazyManThreadSecurity instance2 = LazyManThreadSecurity.getInstance();
        System.out.println(instance1==instance2); //true
    }
}

2.说明

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

三、饿汉式

1.代码

public class Hunger {
    private static Hunger instance= new Hunger();
    //构造器私有
    private Hunger() {
    }
    //提供获取实例方法
    public static Hunger getInstance(){
        return instance;
    }

    //测试
    public static void main(String[] args) {
        Hunger instance1 = Hunger.getInstance();
        Hunger instance2 = Hunger.getInstance();
        System.out.println(instance1==instance2);
    }
}

2.优点

这种方式比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

四、双检锁/双重校验锁

1.代码

public class DoubleCheck {

    private volatile static DoubleCheck instance = null;

    //构造器私有
    private DoubleCheck() {
    }

    //提供获取实例方法
    public static DoubleCheck getInstance() {
        if(instance==null){
            synchronized (DoubleCheck.class){
                instance = new DoubleCheck();
                return instance;
            }
        }
        return instance;
    }

    //测试
    public static void main(String[] args) {
        DoubleCheck instance1 = DoubleCheck.getInstance();
        DoubleCheck instance2 = DoubleCheck.getInstance();
        System.out.println(instance1==instance2);
    }
}

2.说明

这段代码是否看着很熟悉,这其实就是在线程安全的懒汉式上,在instance说明语句上加了volatile关键字

为什么要加volatile呢 ?

  • 因为在执行getInstance方法时,虽然看似是线程安全的,但不一定是安全的。

  • 原因:instance =new DoubleCheck();不是有序性操作。

    • instance =new DoubleCheck();实际上分了三步操作
    • 1.开辟一个内存空间
    • 2.初始化构造器
    • 3.将改对象指向这个空间(这时候这个空间就属于这个对象了)
    • 若以上三步在执行时,是按顺序执行的,那么就没问题。但是以上三步,在CPU执行时顺序可以不同。

    例如:有两条线程A和B,

    A线程先进入执行instance =new DoubleCheck();,而因为某些因素,指令执行是按照132

    当A线程指令执行完3没执行2时B线程也执行了getInstance方法,此时instance对象已经指向某个内存空间,所以instance不等于null,则直接返回了,此时改对象的构造函数还没执行,所以就引发了错误。

  • volatile关键字:作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。(保证指令顺序执行)

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

五、登记式/静态内部类

1.代码

public class StaticSingle {
    //静态内部类
    private static class StaticInner{
        //创建后不可修改
        private static final StaticSingle INSTANCE = new StaticSingle();
    }

    private StaticSingle() {
    }

    public static StaticSingle getInstance(){
        return StaticInner.INSTANCE;
    }
}

2.说明

该方法利用了静态内部类的特性,只有在调用时才会对其初始化。

这种方式能达到双检锁方式一样的功效,但实现更简单。

对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。

这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Hunger 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 StaticInner类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 StaticInner类,从而实例化 instance。

想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

六、枚举

1.代码

public enum  SingleEnum {
    INSTANCE;
}

2.说明

使用时直接,SingleEnum.INSTANCE;就可以得到单例对象了!

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法

它更简洁,自动支持序列化机制,绝对防止多次实例化。(枚举类特性
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

总结

经验之谈:一般情况下,不建议使用第 一 种和第 二 种懒汉方式,建议使用第 三 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 五5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 六 种枚举方式。如果有其他特殊的需求,可以考虑使用第 四 种双检锁方式。

炫技时刻

通过反射破坏单例

1.基础

方法一到方法五都有私有构造器,都可以使用反射生成多个实例

这里就用看起来比较好的方法二举例

public class ReflectOne {
    public static void main(String[] args) throws Exception {
        LazyManThreadSecurity instance1 = LazyManThreadSecurity.getInstance();
        // 获得无参构造器
        Constructor<LazyManThreadSecurity> declaredConstructor = LazyManThreadSecurity.class.getDeclaredConstructor(null);
        // 破坏私有构造方法
        declaredConstructor.setAccessible(true);
        LazyManThreadSecurity instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1 == instance2);
    }
}

输出结果

single.LazyManThreadSecurity@677327b6
single.LazyManThreadSecurity@14ae5a5
false

Process finished with exit code 0

2.level up!

你决定上面反射的方法是需要通过构造器的,所以你觉得可以设置一个标志位

当有实例生成之后,标志位就置为某个值,下次再执行构造函数时,就抛出异常,就不会生成新的对象了

代码改造

public class LazyManThreadSecurity2 {
    private static LazyManThreadSecurity2 instance = null;
    private static boolean created = false;//添加标志位

    private LazyManThreadSecurity2() {
        if(!created){
            created = true;
        }else {
            throw new RuntimeException("别乱搞哦!");
        }
    }

    public static LazyManThreadSecurity2 getInstance(){
        if(instance==null){
            //加锁,只能用类的锁才能保证一个类只有一个实例
            synchronized (LazyManThreadSecurity2.class){
                instance = new LazyManThreadSecurity2();
                return instance;
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        LazyManThreadSecurity2 instance1 = LazyManThreadSecurity2.getInstance();
        LazyManThreadSecurity2 instance2 = LazyManThreadSecurity2.getInstance();
        System.out.println(instance1==instance2); //true
    }
}

测试

public class ReflectOne {
    public static void main(String[] args) throws Exception {
        LazyManThreadSecurity2 instance1 = LazyManThreadSecurity2.getInstance();
        // 获得无参构造器
        Constructor<LazyManThreadSecurity2> declaredConstructor = LazyManThreadSecurity2.class.getDeclaredConstructor(null);
        // 破坏私有构造方法
        declaredConstructor.setAccessible(true);
        LazyManThreadSecurity2 instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1 == instance2);
    }
}

输出

Exception in thread “main” java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at single.promote.ReflectOne.main(ReflectOne.java:19)
Caused by: java.lang.RuntimeException: 别乱搞哦!
at single.promote.LazyManThreadSecurity2.(LazyManThreadSecurity2.java:18)
… 5 more

3.level up!!

增加标志位后就没办法了吗?

反射这么强大怎么可能没办法呢,通过反射将标志位设置一下就好了(可以通过反编译看源码,标志位可能是一个通过加密的值,所以需要多种手段结合)

public class ReflectOne {
    public static void main(String[] args) throws Exception {
        LazyManThreadSecurity2 instance1 = LazyManThreadSecurity2.getInstance();

        //改变标志位
        Field created = LazyManThreadSecurity2.class.getDeclaredField("created");
        created.setAccessible(true);
        created.set("create",false);

        // 获得无参构造器
        Constructor<LazyManThreadSecurity2> declaredConstructor = LazyManThreadSecurity2.class.getDeclaredConstructor(null);
        // 破坏私有构造方法
        declaredConstructor.setAccessible(true);
        LazyManThreadSecurity2 instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1 == instance2);
    }
}

输出

single.promote.LazyManThreadSecurity2@7f31245a
single.promote.LazyManThreadSecurity2@6d6f6e28
false

Process finished with exit code 0

枚举类为什么就是安全的?

尝试通过反射破坏单例

1.通过编译后的结果看到改类有一个私有构造器

在这里插入图片描述

public class EnumReflect {
    public static void main(String[] args) throws Exception {
        SingleEnum instance1 = SingleEnum.INSTANCE;
		//尝试通过私有构造器新建实例
        Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        SingleEnum instance2 = declaredConstructor.newInstance();

        System.out.println(instance1==instance2);
    }
}

输出结果:

Exception in thread “main” java.lang.NoSuchMethodException: single.SingleEnum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at single.promote.EnumReflect.main(EnumReflect.java:18)

  • 报错:“没有这样的构造器”,我们明明可以看到编译后确实有该构造器,且我们已经破坏其private属性。
  • 此时只有一个可能,idea骗了我们!

2.idea骗了我们,那么就用javap命令反编译一下

  • ”右键“该类,选择”Show in Explorer“到该类所在目录下

在这里插入图片描述

  • 在上方输入栏输入”cmd“并回车

在这里插入图片描述

  • 输入命令
#格式:java -p 类名
javap -p SingleEnum.class

在这里插入图片描述

可以看到此时也只是有一个无参构造方法

3.用专业工具反编译

jad下载

jad:把.class文件反编译 成.java文件的专业工具

进入页面,点击”Download Jad“

选择适合自己的版本,以win10为例,就选择以下的版本

在这里插入图片描述

下载不了的可以用以下的仓库下载

仓库

下载完成后

选择1:把jad配置到环境变量Path中,以后在任何地方都可以用jad命令

选择2:把jad.exe复制到要反编译的文件的同目录下,再执行jad命令

我这里采用方法2

# 在命令行中输入以下命令
jad

在这里插入图片描述

4.反编译

①复制到相同目录下,并打开命令行切换到该窗口(快捷方式:回车栏cmd)

要反编译这个SingleEnum.class

在这里插入图片描述

②执行反编译命令

#jad 文件名.class
jad SingleEnum.class

编译成功会看到如下输出

Parsing SingleEnum.class… Generating SingleEnum.jad

同时在该目录下看到一个SingleEnum.jad文件,可以改成.java为后缀后打开(有颜色)或者直接用记事本打开也好。

惊人的一幕发生了!!!该类竟然没有无参构造器,而是一个带两个参数的私有构造器!!!

在这里插入图片描述

5.通过该构造器继续尝试用反射的方法破坏单例

public class EnumReflect {
    public static void main(String[] args) throws Exception {
        SingleEnum instance1 = SingleEnum.INSTANCE;

        Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        SingleEnum instance2 = declaredConstructor.newInstance();

        System.out.println(instance1 == instance2);
    }
}

输出结果:

Cannot reflectively create enum objects

在这里插入图片描述

6.通过源码究原因

①查看newInstance();方法的源码

public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

关键代码

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException(“Cannot reflectively create enum objects”);

如果是枚举,直接就抛异常了!

jdk从根本上拒绝了从反射获得枚举类实例

②Enum的clone和序列化方法

直接抛异常(狠)

protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}

private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}

通过以上的分析就可以得知为什么枚举类是最靠谱实现单例的模式!

补充:

实现单例的最简单的方法就是私有化构造器,但这个方法可以用clone,序列化,反射破坏单例!


学完,一个字,爽!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值