彻底玩转单例模式

彻底玩转单例模式

1 单例模式(Singleton Pattern)简介

1.1 概述

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

注意:

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

1.2 介绍

**意图:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。

**主要解决:**一个全局使用的类频繁地创建与销毁。

**何时使用:**当您想控制实例数目,节省系统资源的时候。

**如何解决:**判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

**关键代码:**构造函数是私有的。

应用实例:

  • 一个班级只有一个班主任。
  • Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  • 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

使用场景:

  • 要求生产唯一序列号。
  • WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

其中,根据加载对象的时机,单例模式有两种表现形式:饿汉式(立即加载)懒汉式(延迟加载)

2 饿汉式(Eager Loading)

饿汉式

概述:立即加载,在虚拟机启动的时候就会创建。在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变。无需关注多线程问题,所以饿汉式的单例对象是线程安全的。

优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。

代码演示:

package com.singleton;

/**
 * 饿汉式
 */
public class MySingletonEagerLoad {

    // 1.私有化构造器
    private MySingletonEagerLoad(){}

    // 2.使用final关键字声明要创建的对象为一个不变的对象,并直接创建对象。浪费内存空间
    private static final MySingletonEagerLoad singleton = new MySingletonEagerLoad();

    // 3.提供外部获取单例的接口
    public static MySingletonEagerLoad getSingletonInstance() {
        return singleton;
    }

}

3 懒汉式(Lazy Loading)

懒汉式

概述:延迟加载,随用随创建。在调用的时候才创建对象,通常情况下是线程不安全的,因为存在对象为null的判断。所以根据线程安全角度,懒汉式可分为:懒汉式-线程不安全懒汉式-线程安全。若在创建实例对象时使用synchronized关键字同步代码块,可使得线程安全。但这并不是绝对的线程安全,仅仅只是在java代码逻辑上是线程安全的,实际上在底层对象的创建是通过3段程序指令来完成的,但这3段指令存在重新排序的情况,使得对象的创建过程并不是绝对意义上的线程安全。若对于共享单例使用volatile关键字修饰,则表明禁止底层指令重排序,可使得线程绝对安全。

3.1 懒汉式-线程不安全

在创建实例对象时没有使用synchronized同步代码,所以是线程不安全的,在多线程下会得到多个实例,不支持多线程。严格意义上不算单例。

代码演示:

package com.singleton;

/**
 * 懒汉式之线程不安全
 *
 * 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
 * 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
 */
public class MySingletonLazyLoad {
	// 1.私有化构造器
    private MySingletonLazyLoad(){}

    // 2.声明要创建的单例对象
    private static MySingletonLazyLoad singleton;

    /**
     * 3.提供外部获取单例的接口(线程不安全)
     * 
     * @return
     */
    public static MySingletonLazyLoad getSingletonInstanceNotSafe() {
        if (singleton == null) {
            singleton = new MySingletonLazyLoad();
        }
        return singleton;
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(MySingletonLazyLoad.getSingletonInstanceNotSafe());
            }).start();
        }
    }

输出结果(当结果都一致时,尽可能加大循环次数,或者加大睡眠时间,后面的测试也都如此):

com.singleton.MySingletonLazyLoad@4f667964
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32

3.2 懒汉式-线程安全

线程安全的体现可分为3种实现方式:

  1. 使用关键字synchronized同步代码块(对于DCL懒汉式需要深究)
  2. 使用静态内部类
3.2.1 synchronized同步代码块的方式

多线程并发情况下,性能消耗较大。加锁是一个很耗性能的操作。

根据能耗分为:非DCL模式DCL模式(Double-Check Lock)

3.2.1.1 非DCL模式

代码演示:

package com.singleton;

/**
 * 懒汉式之线程安全
 * 非DCL模式
 * 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
 * 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
 */
public class MySingletonLazyLoad {
	// 1.私有化构造器
    private MySingletonLazyLoad(){}

    // 2.声明要创建的单例对象
    private static MySingletonLazyLoad singleton;
    
    /**
     * 3.提供外部获取单例的接口(线程安全,但很耗性能)
     * 
     * @return
     */
    public static synchronized MySingletonLazyLoad getSingletonInstanceSafe() {
        if (singleton == null) {
            singleton = new MySingletonLazyLoad();
        }
        return singleton;
    }
    
    // 多线程测试
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(MySingletonLazyLoad.getSingletonInstanceSafe());
            }).start();
        }
    }
}

输出结果

com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
3.2.1.2 DCL模式(Double-Check Lock)

DCL(Double-Check Lock):即双重校验锁。是两层针对单例是否为null的检查,中间嵌套一个同步锁的结构形式。目的是为了降低由同步所带来的性能消耗。使得只是在创建单例的时候有同步锁带来的性能消耗,多线程下再次获取单例对象的时候就会先被外层null检查拦截而不进入同步锁。

代码演示:

package com.singleton;

/**
 * 懒汉式之线程安全
 * DCL模式
 * 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
 * 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
 */
public class MySingletonLazyLoad {
	// 1.私有化构造器
    private MySingletonLazyLoad(){}

    // 2.声明要创建的单例对象,同时声明为volatile,表明禁止指令重排序
    private volatile static MySingletonLazyLoad singleton;
    
    /**
     * 3.提供外部获取单例的接口(线程安全,优化同步,降低能耗)
     * 采取方案:双重校验锁(Double Check Lock) + volatile
     * 
     * @return
     */
    public static MySingletonLazyLoad getSingletonInstanceSafe() {
        if (singleton == null) {
            synchronized(MySingletonLazyLoad.class) {
                if (singleton == null) {
                    /**
                     * 对象创建的流程(期望):
                     * 1、分配内存空间
                     * 2、调用构造器初始化对象
                     * 3、对象指向这个内存空间
                     * 指令重排序:这并不是一个原子性的操作,123的执行过程可能会存在置换。
                     * 当线程A正在执行13---------------2的过程中创建对象时
                     *   线程B发现此时对象还是null,而实际上线程A只是在加载过程中并很快创建了该对象,此时线程B的访问就是有问题的。
                     */
                    singleton = new MySingletonLazyLoad(); //对象的创建过程本不是一个原子性的操作,但通过volatile禁止了指令重排序,使得线程是绝对安全的
                }
            }
        }
        return singleton;
    }
    
    // 多线程测试
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(MySingletonLazyLoad.getSingletonInstanceSafe());
            }).start();
        }
    }
}

输出结果:

com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
3.2.2 静态内部类的实现方式

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

MySingletonLazyLoad类被装载了,singleton不一定被初始化。因为 InnerSingleton类没有被主动使用,只有通过显式调用 getSingletonInstanceSafe方法时,才会显式装载 InnerSingleton类,从而实例化 singleton。

代码演示:

package com.singleton;

/**
 * 懒汉式
 *
 * 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
 * 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
 */
public class MySingletonLazyLoad {
	// 1.私有化构造器
    private MySingletonLazyLoad(){}
    
    /**
     * 3.提供外部获取单例的接口(线程安全,不耗性能)
     * 返回静态内部类中的单例对象
     * @return
     */
    public static MySingletonLazyLoad getSingletonInstanceSafe() {
        return InnerSingleton.singleton;
    }
    
    /**
     * 2.声明一个静态内部类
     * 用于立即加载单例对象
     */
    public static class InnerSingleton {
        // 在静态内部类中声明为立即加载的单例,是线程安全的
        private static final MySingletonLazyLoad singleton = new MySingletonLazyLoad();
    }
    
    // 多线程测试
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(MySingletonLazyLoad.getSingletonInstanceSafe());
            }).start();
        }
    }
}

输出结果:

com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647

4 单例模式的安全问题

单例模式并不安全,可用java的反射机制或反序列化机制破坏其单例性质

4.1 反射机制下对单例的破坏演示

public static void main(String[] args) throws Exception{
        MySingletonLazyLoad instance1 = MySingletonLazyLoad.getSingletonInstanceSafe();
        Constructor<MySingletonLazyLoad> declaredConstructor = MySingletonLazyLoad.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        MySingletonLazyLoad instance2 = declaredConstructor.newInstance();
        MySingletonLazyLoad instance3 = declaredConstructor.newInstance();
        System.out.println(instance1); //com.singleton.MySingletonLazyLoad@1540e19d
        System.out.println(instance2); //com.singleton.MySingletonLazyLoad@677327b6
        System.out.println(instance3); //com.singleton.MySingletonLazyLoad@14ae5a5
    }

输出结果:

com.singleton.MySingletonLazyLoad@1540e19d
com.singleton.MySingletonLazyLoad@677327b6
com.singleton.MySingletonLazyLoad@14ae5a5

很明显,输出结果都不一致!反射对单例的破坏是很致命的!

4.2 反射破坏单例的解决方案

4.2.1 私有构造器中构造异常(治标)

例如:在私有构造器中添加静态标记位(可加密,不易被外界所发现)

   private static boolean flag = false;
    private MySingletonLazyLoad(){
        synchronized (MySingletonLazyLoad.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("不要试图使用反射破坏单例结构!");
            }
        }
    }

但是依然存在破解的可能性!该标记位即便加密也有被解密的风险,反射同样可以获取到该值进一步去破解!所以这种方式治标不治本。

4.2.2 使用枚举类 (治本)

实际上,实现单例模式的最佳方法就是枚举Enum。它更简洁,自动支持序列化机制,绝对防止多次实例化。它不仅能避免多线程同步问题(也是立即加载),而且还自动支持序列化机制防止反序列化重新创建新的对象,绝对防止多次实例化。不能通过 reflection attack 来调用私有构造方法实例化。通过反射调用构造器实例化的时候,底层会判断该类型是否为枚举,若是则抛出异常,从而达到单例的绝对有效。

例如:定义一个枚举类Payment,声明多个单例对象

package com.singleton;

public enum Payment {
    CASH("现金支付"),
    WECHAT_PAY("微信支付"),
    ALIPAY("阿里支付"),
    BANK_CARD("银行卡支付"),
    CREDIT_CARD("信用卡支付");

    private String paymentDesc;

    Payment() {}

    Payment(String paymentDesc){
        this.paymentDesc = paymentDesc;
    }

    public String getPaymentDesc() {
        return paymentDesc;
    }
}

使用反射机制打算破坏其单例结构

public static void main(String[] args) throws Exception {
     Constructor<Payment> paymentConstructor = Payment.class.getDeclaredConstructor(String.class);
        paymentConstructor.setAccessible(true);
        Payment payment = paymentConstructor.newInstance();
        System.out.println(payment);
}

输出结果:

Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.Payment.<init>(java.lang.String)
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.singleton.MainTest.main(MainTest.java:2)6)

打印出了异常信息,但是控制台显示的异常很诡异:NoSuchMethodException: com.singleton.Payment.(java.lang.String)。这是不合理的。因为在自定义的枚举类Payment中很明显的声明了私有的带参构造器,而这里却显示异常为NoSuchMethodException:没有找到此构造器。。。

这是因为idea编译器骗了我们!使用第三方正规的反编译器jad进行反编译(这里使用的是图形化jad反编译工具FrontEnd Plus):

// Decompiled Using: FrontEnd Plus v2.03 and the JAD Engine
// Available From: http://www.reflections.ath.cx
// Decompiler options: packimports(3) 
// Source File Name:   Payment.java

package com.singleton;


public final class Payment extends Enum
{

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

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

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

    private Payment(String s, int i, String paymentDesc)
    {
        super(s, i);
        this.paymentDesc = paymentDesc;
    }

    public String getPaymentDesc()
    {
        return paymentDesc;
    }

    public static final Payment CASH;
    public static final Payment WECHAT_PAY;
    public static final Payment ALIPAY;
    public static final Payment BANK_CARD;
    public static final Payment CREDIT_CARD;
    private String paymentDesc;
    private static final Payment $VALUES[];

    static 
    {
        CASH = new Payment("CASH", 0, "\u73B0\u91D1\u652F\u4ED8");
        WECHAT_PAY = new Payment("WECHAT_PAY", 1, "\u5FAE\u4FE1\u652F\u4ED8");
        ALIPAY = new Payment("ALIPAY", 2, "\u963F\u91CC\u652F\u4ED8");
        BANK_CARD = new Payment("BANK_CARD", 3, "\u94F6\u884C\u5361\u652F\u4ED8");
        CREDIT_CARD = new Payment("CREDIT_CARD", 4, "\u4FE1\u7528\u5361\u652F\u4ED8");
        $VALUES = (new Payment[] {
            CASH, WECHAT_PAY, ALIPAY, BANK_CARD, CREDIT_CARD
        });
    }
}

这里可以很清晰的发现每个单例对象的创建都是调用的

private Payment(String s, int i, String paymentDesc)
{
super(s, i);
this.paymentDesc = paymentDesc;
}

这个构造器,所以在idea中报了找不到初始化参数为java.lang.String的构造器的异常。

于是可以预见的,我们通过反射获取正确参数类型的构造器:

public static void main(String[] args) throws Exception {
     Constructor<Payment> paymentConstructor = Payment.class.getDeclaredConstructor(String.class,int.class,String.class);
        paymentConstructor.setAccessible(true);
        Payment payment = paymentConstructor.newInstance();
        System.out.println(payment);
}

输出结果:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.singleton.MainTest.main(MainTest.java:4)

异常信息为IllegalArgumentException: Cannot reflectively create enum objects。无法通过反射创建枚举类型的实例对象。所以通过枚举构建的单例是无法通过反射来破坏的。这是因为在底层Constructor.java中明确声明了对于枚举类通过反射来创建实例对象会抛出异常!

@CallerSensitive
    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;
    }

4.3 小结

通过反射机制可以破坏自定义的单例模式(非枚举类),可以在私有构造器中加入一定的逻辑判断(添加标记位)有效阻止这种破坏,但是无法从根源上进行阻止,这是由于反射机制可以随便更改类中的任何信息定义。要从根源上解决这种问题,必须采用JDK5提供的枚举类Enum。因为在底层就已经避免了反射机制的入侵!

5 总结

**经验之谈:**一般情况下,不建议使用3.1和3. 2 .1.1的懒汉方式,建议使用第 2种饿汉式。只有在要明确实现 lazy loading 效果时,才会使用3.2.2 的登记方式/静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用4.2.2介绍的枚举方式。如果有其他特殊的需求,可以考虑使用3.2.1.2双检锁方式。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值