设计模式之单例模式详解(java)

Posted 小样5411

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之单例模式详解(java)相关的知识,希望对你有一定的参考价值。

一、单例模式

单例模式的实现方式有许多种,重点是以下五种:饿汉式、懒汉式、双重校验锁(DCL懒汉式)、静态内部类,枚举

单例模式特点:
a)只能有一个实例(getInstance获得的实例要一致)
b)必须自己创建自己的唯一实例(构造器私有,只能所在类自己创建)
c)必须给所有其他对象提供这一实例(public修饰getInstance方法)

1.1 饿汉式

饿汉式的意思就是一上来就加载,也就是随着类加载而加载,不管是否真的会使用。饿汉式中一个重要思想是构造器私有,单例一定要有构造器私有,构造器私有就是保证内存中只有一个对象,只能在本类Hungry中new,别处就无法new这个对象,这样就更为安全,有点像对属性的私有化、封装。

//饿汉式单例
public class Hungry {

    //一上来就会加载,但一直没有使用,可能会造成空间浪费
    private byte[] data = new byte[1024*1024];

    //构造器私有化(重点)
    private Hungry(){

    }

    private final static Hungry HUNGRY = new Hungry();

    public Hungry getInstance(){
        return HUNGRY;
    }
}

但这种单例存在一种问题,也是饿汉式的问题,一上来就加载,如果从始至终从未使用过这个实例,则会造成内存的浪费。

所以我们介绍懒汉式,懒汉式的意思就是用的时候再加载,不用就不加载,这样就不会造成内存的浪费。

注意:JVM中只有5种情况会对类进行初始化
1、new一个关键字,如new String()
2、一个实例化对象,如new Student()
3-4、读取或设置一个静态字段(final修饰,已经编译初始化的常量除外,即final修饰的只会初始化加载一次)
5、调用一个类的静态方法

上面由于编译时读取到final关键字,于是就会随着类加载而加载

1.2 懒汉式

懒汉式就是使用时再调用getInstance()获得实例对象,这就可以避免浪费

//懒汉式单例
public class LazyMan {

    //构造器私有化
    private LazyMan(){

    }

    private static LazyMan lazyMan;

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

上面确实实现了懒汉式单例,但是存在线程不安全问题,单线程没问题,但多线程就会出现不安全问题,如下多线程并发执行

//懒汉式单例
public class LazyMan {

    //构造器私有化
    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + "->OK");
    }

    private static LazyMan lazyMan;

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

    //多线程并发,获取同一个资源,即同时调用getInstance()
    public static void main(String[] args) {
        for (int i = 0; i < 8; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

执行了四次每次结果都不一样,甚至有些线程都没有执行,没有获取到资源,每次结果不一样肯定是线程不安全的。

如何解决???可以加锁,用双重检验锁,也称DCL懒汉式,DCL全称(Double Check Lock)

1.3 DCL懒汉式(双重检验锁)

//懒汉式单例
public class LazyMan {

    //构造器私有化
    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + "->OK");
    }

    private static LazyMan lazyMan;

    //双重检测锁(双重的意思的两个lazyMan == null,锁就是锁它的类 .class)
    //简称DCL懒汉式
    public static LazyMan getInstance(){
        //对LazyMan类进行加锁
        if (lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    //多线程并发,获取同一个资源,即同时调用getInstance()
    public static void main(String[] args) {
        for (int i = 0; i < 8; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

然后运行,就只会一个获取到锁,怎么运行都是一个,每次结果都一样,加锁就保证了线程安全

但还存在问题,就是getInstance()方法中的lazyMan = new LazyMan();不是原子性操作,new LazyMan()底层其实有三个操作
1、分配内存空间指令
2、执行构造方法,初始化对象指令
3、将对象引用指向这个分配的空间指令

我们知道程序在编译时会进行指令重排,可能会出现132, 231情况,虽然概率很小,但理论存在,为了防止指令重排可能带来的影响,我们就要加一个volatile关键字,完整的DCL懒汉式如下,也可以说是优化的DCL(Double Check Lock)

//懒汉式单例
public class LazyMan {

    //构造器私有化
    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + "->OK");
    }

	//禁止指令重排
    private volatile static LazyMan lazyMan;

    //双重检测锁(双重的意思的两个lazyMan == null,锁就是锁它的类 .class)
    //简称DCL懒汉式
    public static LazyMan getInstance(){
        //对LazyMan类进行加锁
        if (lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    //多线程并发,获取同一个资源,即同时调用getInstance()
    public static void main(String[] args) {
        for (int i = 0; i < 8; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

1.4 静态内部类

也可以用静态内部类实现,静态内部类也是线程安全的,因为JVM中的类加载器会自动负责加锁,保证线程安全,之前已经说明了,JVM中只有5种情况会对类进行初始化
1、new一个关键字,如new String()
2、一个实例化对象,如new Student()
3-4、读取或设置一个静态字段(final修饰,已经编译初始化的常量除外,即final修饰的只会初始化加载一次)
5、调用一个类的静态方法

外部类加载时并不会立即加载内部类,因为不属于上面的5种情况,所以内部类不被加载则不去初始化实例,故而不占内存,只有调用了getInstance时JVM才会初始化。这也称为懒加载,不会随着外部类加载一并加载,只有调用静态内部类的时候才会加载,并且静态内部类不用担心指令重排问题,因为JVM初始化时能保证不会发生这个问题。

优势总结:相比DCL代码简洁,线程安全,相比饿汉式不会有空间浪费

public class Holder {

    //构造器私有化
    private Holder(){

    }
	//获取单例 public
    public static Holder getInstance(){
        System.out.println(Thread.currentThread().getName() + "-> Ok");
        return InnerClass.HOLDER;
    }

    //静态内部类
    public static class InnerClass{
    	//final修饰,不允许改变
        private static final Holder HOLDER = new Holder();
    }
}

1.5 枚举

为什么讲枚举???因为上面讲的虽然可以保证线程安全、不会浪费空间,但是可以用神器反射!!! 破解,因为反射连私有private关键字都可以获取,使得整个类都暴露出来

//懒汉式单例
public class LazyMan {

    //构造器私有化
    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + "->OK");
    }

    private volatile static LazyMan lazyMan;

    //双重检测锁(双重的意思的两个lazyMan == null,锁就是锁它的类 .class)
    //简称DCL懒汉式
    public static LazyMan getInstance(){
        //对LazyMan类进行加锁
        if (lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    //反射破坏单例
    public static void main(String[] args) throws Exception {
        //获取实例
        LazyMan instance = LazyMan.getInstance();
        //获取LazyMan的无参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //设置在使用构造器的时候不执行权限检查,从而不会进行设置的private权限
        declaredConstructor.setAccessible(true);
        //用反射new对象
        LazyMan instance1 = declaredConstructor.newInstance();
        //两个实例不一致,于是就破坏了单例
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());
        System.out.println(instance == instance1);//fasle
    }
}

执行结果

结论:反射能破坏单例,导致不是一个实例

所以引出枚举,枚举就不会被反射给破坏!!!

不太懂枚举,可以看看这个,一个很厉害的博主

面试官:为啥需要枚举?枚举有什么作用?怎么用枚举实现单例?
这篇文章讲的很明白,如为什么用枚举,用静态变量形式不行吗?给出如下回答,感兴趣可以仔细看看这篇好文,我们这里主要说,枚举实现的单例不会被反射破坏。

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        EnumSingle instance = EnumSingle.INSTANCE;
        EnumSingle instance1 = EnumSingle.INSTANCE;
        System.out.println(instance == instance1);
    }
}

执行结果true,说明用枚举获得的实例就是单例,枚举本身自带实现单例

我们再试试用反射创建实例

public static void main(String[] args) throws Exception {
        EnumSingle instance = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance1 = declaredConstructor.newInstance();//反射创建实例
        System.out.println(instance == instance1);
    }
}

会抛出异常,说明无法破坏

本文参考的视频讲解:https://www.bilibili.com/video/BV1K54y197iS

以上是关于设计模式之单例模式详解(java)的主要内容,如果未能解决你的问题,请参考以下文章

设计模式之单例模式

详解Java设计模式之单例模式(Singleton Pattern)

设计模式之单例模式详解和应用

kotlin 之单例类详解

Java设计模式之单例模式

Java设计模式——创建型模式之单例模式