2020最新单例模式详解,希望你看了有收获

注: 看一遍只能有个印象,想学会还要自己敲一遍(第四节反射选看)

1. 什么是单例模式?

  • 单例模式强调一个单字,保证每个类仅有一个实例,访问这个类的线程共用一个对象。

2. 看看它的几种实现方式

2.1 最简单的饿汉式

饿汉式,顾名思义,它很“饿”啊,就想一口吃个胖子,很急,在类加载的时候就创建了实例对象,如下方代码所示,还没有调用getInstance方法,实例对象就给你准备好了,你说急不急?

public class Hungry {
    
    //私有构造器,避免外部创建对象
    private Hungry(){}
    
    //实例对象(类加载的时候就创建了实例对象)
    private static final Hungry HUNGRY = new Hungry();
    
    //获取单例的方法
    public static Hungry getInstance(){
        return HUNGRY;
    }
}
  • 它的弊端也很明显,在类加载的时候就实例化了对象,浪费内存,线程也不安全。
  • 为了解决这个不在类加载的时候就初始化对象的问题,就出现了下面的懒汉式,我们接着看。

3. 懒汉式

3.1 简单的懒汉式

下面的代码解决了饿汉式浪费内存的问题,但是线程还是不安全的,我们在3.2节中进行线程安全改进。

public class Lazy01 {
    //私有构造器
    private Lazy01(){

    }

    //没有实例化
    private static Lazy01 LAZY ;
    
    public static Lazy01 getInstance(){
        if(LAZY == null){
            LAZY = new Lazy01();
        }
        return LAZY;
    }
}
  • 你说线程不安全就不安全?我非要检测一下试试!
public class Lazy01 {
    //私有构造器
    private Lazy01(){
        System.out.println(Thread.currentThread().getName() + "获取实例");
    }

    //没有实例化
    private static Lazy01 LAZY ;

    public static Lazy01 getInstance(){
        if(LAZY == null){
            LAZY = new Lazy01();
        }
        return LAZY;
    }

    public static void main(String[] args) {
		//创建10个线程,来获取单例类的实例
        for(int i = 0;i < 10;i++){
            new Thread(()->Lazy01.getInstance()).start();
        }
    }
}

我们看输出,两次输出不相同,线程不安全
在这里插入图片描述
在这里插入图片描述

3.2 解决线程安全问题的懒汉式

我们在 getInstance()方法上,添加synchronized关键字,就解决了线程安全

    public static synchronized Lazy01  getInstance(){
        if(LAZY == null){
            LAZY = new Lazy01();
        }
        return LAZY;
    }
  • 但是我们发现,用了 synchronized 关键字修饰,效率很慢,为了解决效率很慢,我们又引入了双重检测锁的懒汉式。

3.3 双重检测锁 DLC懒汉式

我们加了一个双重检测锁,保证了单一线程创建实例,又提高了效率

    public static  Lazy01  getInstance(){
        //LAZY == null的时候把这个类锁起来,保证只有一个线程访问
        if(LAZY == null){
            synchronized (Lazy01.class){
                //这里再创建实例
                if(LAZY == null){
                    LAZY = new Lazy01();
            }
        }
        }
        return LAZY;
    }
  • 但是你以为加了双重检测锁就完了嘛?太天真了吧
  • 我们还需要做一点儿小细节的改进。
 LAZY = new Lazy01(); // 创建对象的过程并不是原子性的,他的底层有三个步骤

// 1. 分配内存空间
// 2. 执行构造方法,初始化实例对象
// 3. 将对象指向这块儿空间

若底层按照正常的顺序,123执行的话,没有问题
假如没有按照123,而是132这种情况,我们想一下
线程A 按照132创建实例对象
同时呢,线程B 也进来了,当A执行到3这个步骤的时候,还没有进行初始化对象,但是已经指向了分配的内存空间
但是!问题来了!
B线程会认为 LAZY 不为空,直接 return,那么这就返回了一个没有初始化的LAZY对象。

解决方法也很简单,加上一个volatile修饰,如下
private static volatile Lazy01 LAZY;
  • 利用静态内部类,也可以实现双重锁的功能

3.4 静态内部类实现

这种情况是,只有显示的调用 getInstance() 方法的时候,才能对内部类进行加载,否则也是不进行初始化的

public class Lazy02 {

    //私有无参构造
    private Lazy02(){

    }

    private static class InnerLazy{
        private static final Lazy02 LAZY = new Lazy02();
    }

    public static Lazy02 getInstance(){
        return InnerLazy.LAZY;
    }
}
  • 但是以上介绍的都是能被反射破解的,下面我们再简单聊聊反射破解吧

4. 反射破解DLC懒汉式单例(选看)

import java.lang.reflect.Constructor;

public class Lazy01 {
    //私有构造器
    private Lazy01(){

    }

    //没有实例化
    private static volatile Lazy01 LAZY ;

    public static  Lazy01  getInstance(){
        //LAZY == null的时候把这个类锁起来,保证只有一个线程访问
        if(LAZY == null){
            synchronized (Lazy01.class){
                //这里再创建实例
                if(LAZY == null){
                    LAZY = new Lazy01();
            }
        }
        }
        return LAZY;
    }
    
	//重点看这一部分代码!!!
    public static void main(String[] args) throws Exception {

        //正常创建对象
        Lazy01 lazy01 = Lazy01.getInstance();

        //获取无参构造
        Constructor<Lazy01> declaredConstructor = Lazy01.class.getDeclaredConstructor();
        //破坏构造函数的私有性
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        Lazy01 lazy02 = declaredConstructor.newInstance(null);


        //下面我们打印一下,看看它是否是一个对象
        System.out.println(lazy01);
        System.out.println(lazy02);
    }
}

我们可以发现呐,打印的是两个不同的对象,这就违背了单例模式的原则!
在这里插入图片描述

  • 那我们如何解决呢?

4.1 再加一把锁来解决反射问题

我们在私有构造器下,加上如下代码

    //私有构造器
    private Lazy01(){
        synchronized (Lazy01.class){
            if(LAZY != null){
                throw new RuntimeException("不要试图使用反射来创建对象");
            }
        }
    }

当我们再运行反射创建对象的时候,就会报错
在这里插入图片描述

  • 但是如果我们两个对象都是通过反射创建的呢?又出现问题了。我们接着看。

4.2 用反射创建两个对象又出现了问题

这里我们不调用 getInstance() 方法创建对象,而是都用反射,我们来看代码

    public static void main(String[] args) throws Exception {

        //正常创建对象
        //Lazy01 lazy01 = Lazy01.getInstance();

        //获取无参构造
        Constructor<Lazy01> declaredConstructor = Lazy01.class.getDeclaredConstructor();
        //破坏构造函数的私有性
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        Lazy01 lazy02 = declaredConstructor.newInstance(null);
        Lazy01 lazy03 = declaredConstructor.newInstance(null);


        //下面我们打印一下,看看它是否是一个对象
        //System.out.println(lazy01);

		//用两次反射创建对象
        System.out.println(lazy02);
        System.out.println(lazy03);
    }

我们可以发现,它又能创建两个不同的对象了
在这里插入图片描述

  • 那么这个又怎么解决呢???

4.3 解决4.2出现的问题

我们添加一个参数进行判断,下面是对代码的修改

    //我们再这里加上一个flag来判断
    private static boolean flag = false;

    //私有构造器
    private Lazy01(){
        synchronized (Lazy01.class){
            //再没有创建对象的时候为false,创建过了就为true,这样来阻止对象创建
            if(flag == false){
                flag = true;
            }else {
                throw new RuntimeException("不要试图使用反射来创建对象");
            }
        }
    }

我们再来测试一下,发现可行了
在这里插入图片描述

  • 但是,这样就真的解决了反射的问题吗?实际上没有

4.4 反射问题的再次出现

假设我们知道了flag字段的名字,能够通过反射进行获取

    public static void main(String[] args) throws Exception {

        //正常创建对象
        //Lazy01 lazy01 = Lazy01.getInstance();

        //获取无参构造
        Constructor<Lazy01> declaredConstructor = Lazy01.class.getDeclaredConstructor();
        //破坏构造函数的私有性
        declaredConstructor.setAccessible(true);
        
        //第一次用反射创建对象
        Lazy01 lazy02 = declaredConstructor.newInstance(null);

        //假设我们知道了字段的名字,来获取这个字段
        Field flag = Lazy01.class.getDeclaredField("flag");
        //破坏字段的私有性质
        flag.setAccessible(true);

        //创建完成一次对象后,把flag的值又改为false
        flag.set(lazy02,false);
        //第二次用反射创建对象
        Lazy01 lazy03 = declaredConstructor.newInstance(null);


        //下面我们打印一下,看看它是否是一个对象
        //System.out.println(lazy01);
        System.out.println(lazy02);
        System.out.println(lazy03);
    }
  • 那这样下去,岂不是道高一尺魔高一丈,就停不下来了???
  • No,不是的,我们还有最终技能,枚举

5. 枚举单例模式

枚举类解决了反射的问题

public enum  EnumSingleton {

    INSTANCE;

    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}
  • 好了,到这里我们就说完了,希望你能有收获,有问题也欢迎指出
    在这里插入图片描述

参考

单例模式 | 菜鸟教程
狂神说Java

原创文章 34 获赞 8 访问量 1158

猜你喜欢

转载自blog.csdn.net/qq_46225886/article/details/105723042