单例模式与进阶

一、初识单例模式

定义

单例模式确保了某个类中只存在唯一的实例,并且自行实例化向整个系统提供这个实例。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。常用于计算机系统中线程池、缓存、日志对象、对话框、打印机、显卡驱动程序对象的设计。

特点

  • 单例类只有一个实例
  • 单例模式必须自己创建自己的唯一对象,必须构造器私有防止外部创建对象!
  • 单例模式必须提供这一实例给所有其他对象

单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。

二、存在的问题

多线程模式下使用单例模式需要格外的小心!

如果唯一实例未创建,有两个线程同时调用创建方法,那么他们同时会监测出没有唯一的实例而创建实例,这样会导致两线程创造多个单例实例!违反了单例模式中实例唯一的原则!

解决方法:为知识类是否已经实例化的变量提供一个互斥锁。

三、单例模式八种写法

1、饿汉式(静态常量)【可以使用】

class Hungry{
    // 唯一实例
    private static final Hungry HUNGRY = new Hungry();
    // 不允许外围创建,构造器私有
    private Hungry(){}
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

优点:写法较简单,在类装载时完成了加载,不会出现刚刚提到的线程问题。

缺点:在类装载时就完成了加载,可能调用该类的其他方法或静态属性就会完成加载,没有达到Lazy Loading的效果,如果从始至终都没有使用该实例,就会出现内存资源的浪费


2、饿汉式(静态代码块)【可以使用】

class Hungry {
    private static final Hungry INSTANCE;
    static {
        INSTANCE = new Hungry();
    }
    private Hungry() {}
    public static Hungry getInstance() {
        return INSTANCE;
    }
}

实现方法与第一种方法相同,因此优缺点也与方法一一致。


3、懒汉式(线程不安全)【不可用】

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

优点:起到了Lazy Loading的效果,能够做到懒加载节约内存资源。

缺点:很明显在多线程环境下两个线程同时进入if(singleton == null)语句就会创建多个实例。


4、懒汉式(线程安全,同步方法)【不推荐】

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

优点:解决了第三种实现中的线程不安全问题,同时也能够解决Lazy Loading问题。

缺点:效率太低了,每一次调用getInstace方法都需要同步。本来在第一次调用后直接return就可以,但还是要进行同步特别影响效率。


5、懒汉式(线程不安全,同步代码块)【不可用】

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

这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。


6、双重检查(DCL懒汉式)【推荐】

public class Singleton {

    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

Double-Check概念通过两次if(singleton == null)检查,八正了线程安全,同时实例化代码只会执行一次,后面再次访问,if判断后会直接返回单例对象。

优点:线程安全;lazy loading;效率很高;

单例类实例为什么需要添加volatile

JUC中学过指令重排,CPU可能会将原本顺序执行的指令执行顺序打乱,但打乱后指令执行结果不会受到影响,就是指令重排。对于new Singleton()这个操作本身就不是一个原子性操作,可以分为以下三步

  1. 分配内存空间,此时该空间内容为空
  2. 执行构造方法,初始化对象修改内存空间内容
  3. 将这个对象指向这个空间,此时sinleton指针被修改为非空地址

正常情况下按照1 -> 2 -> 3的顺序执行与按照1 -> 3 -> 2顺序执行结果不会被影响,因此CPU可能会将指令重排使得执行次序为1 -> 3 -> 2
假设有进程A、B,A进程按照1 -> 3 -> 2 顺序执行到 3 时,此时singleton指向了一个未添加内容的内存地址,即:singleton非空,但是singleton指向地址存储值为空。B进程进入函数后进行if(singleton==null)判断,判断为false直接返回singleton,此时返回的引用指向的地址因为进程A还没有执行到 2 操作所以没有值,可能会引发错误。

因此为了防止指令重排带来的一系列问题,该方法创建单例类实例需要添加volatile防止指令重排。

7、静态内部类【推荐】

public class Singleton {

    private Singleton() {}
	
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

对于SingletonInstace只有在调用getInstace()时才会被加载完成实例化,相较于饿汉式这样的加载方式能够节省内存空间,确保了lazyLoading。

同时静态属性只有第一次类加载时才会初始化,所以JVM也能保证线程的安全性。

优点:线程安全;lazyLoading;效率高


8、枚举【特别推荐】

为什么是枚举?

前7种方法实现单例模式中,都忽略了Java中一个重要创建对象的方法,反射

通过反射能够随意修改单例类构造器的访问权限并创建对象,几乎没有方法能够防止反射创建单例模式的实例对象。因此前面7中方法或多或少都会有弊端。

但是反射无法破坏枚举类,在newInstance方法中有这么一个判断,使得使用枚举类实现的单例模式不会受到反射的影响!

image-20220203212420867

枚举类的所有成员均只有唯一实例,与静态类、静态成员变量相同,都是使用JVM特性实现单例模式。那么现在尝试实现单例模式:

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }

}
public static void main(String []args){
    Singleton.INSTANCE.doSomthing()
}

如果仍然想通过反射破坏枚举类,通过javap -p .class文件反编译 .class 文件可以得知(可能会得到无参构造器,但是使用更高级的反编译器jad.exe反编译会得到正确构造器),枚举类中拥有一个双参构造器

image-20220203220314079

使用反射尝试强行创建实例破坏单例

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
    declaredConstructor.setAccessible(true);
    Singleton singleton = declaredConstructor.newInstance();
    singleton.doSomething();
}

得到结果:抛出异常

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
	at Main.main(Main.java:15)

可以看到使用Enum实现的单例模式能够防治反射恶意破坏!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

秋刀鱼与猫_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值