单例模式之懒汉式

2023-10-27

       在上篇文章中,我们讲了单例模式中的饿汉式,今天接着来讲懒汉式。

1.懒汉式单例模式的实现

public class LazySingleton {

    private static LazySingleton instance = null;

    // 让构造函数为private,这样该类就不会被实例化
    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    public long getRamAddress() {
        return this.hashCode();
    }
}

      懒汉式的特点在于有需要时才实例化。

2.懒汉式线程安全

      在多线程的环境下,getInstance方法会导致线程不安全。因为在getInstance时,可能A、B两个线程几乎同时进入,在A实例化未完成的情况下,B判断实例仍然为null,因此继续实例化,这样有实例化了两个不同的对象,明显违背了单例的初衷。这里可以稍微验证一下线程安全问题,为了方便验证,在getInstance让当前线程sleep:

    public static LazySingleton getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new LazySingleton();
        }
        return instance;
    }

       测试代码:

      /**
     * 公共方法,在多线程环境下测试单例,避免重复编写测试代码
     * @param threadCount 线程数
     * @param func        函数,用于获取单例
     * @param <T>
     */
    public static <T> void singLetonMultiThread(int threadCount, Supplier<T> func) {
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        IntStream.range(0, threadCount).forEach(i -> {
            executorService.submit(() -> {
                System.out.println(func.get());
            });
        });

        // 等线程全部执行完后关闭线程池
        executorService.shutdown();
        try {
            executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void lazySingLetonTest() {
        Supplier func = () -> LazySingleton.getInstance();
        singLetonMultiThread(10, func);
    }

测试结果如下:

  

       十个线程里出现了8个不同的对象!

2.1 双重检查锁

      我们需要改进一下getInstance方法,考虑到效率问题,我们不想直接在getInstance方法上加锁,因为这种方式下每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。于是,我们得在判断实例是否为空时加锁,并且进行双重检查:

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                // 这里需要再判断一次,因为可能有其它线程已经创建实例
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

       这样看起来似乎是解决了问题了。

2.2 指令重排

       但这还有瑕疵。对于:

instance = new LazySingleton();

       这个创建对象的语句其实是个非原子操作,在极端的多线程环境下,会存在安全问题。对象的创建过程,在执行的时候分解成以下三条指令:

memory=allocate(); 			//1.分配对象的内存空间
ctorInstance(memory);       //2.执行构造方法来初始化对象
instance=memory;			//3.设置instance指向刚分配的内存地址

       正常执行顺序应该是1->2->3,但可能指令会被重排序为1->3->2,也就是说,2、3步有可能发生指令重排导致重排序,因为synchronized只能保证有序性,但无法禁止指令重排。假设有两个线程A、B,在双重检查锁内,从cpu时间片上的执行顺序如下:

       A线程执行完3还没执行2,虽然分配了内存空间已,但是还没初始化对象,而此时B线程进来判断(instance == null),由于instance已经指向了内存空间,所以instance != null,于是直接返回了对象,但此时对象还未初始化。这样一来,线程B将得到一个还没有被初始化的对象。

       为了防止指令重排,需在声明instance对象时加上volatile关键词:

public class LazySingleton {

    // volatile,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
    private static volatile LazySingleton instance = null;

    // 让构造函数为private,这样该类就不会被实例化
    private LazySingleton() {
        //System.out.println("懒汉式单例初始化!");
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                // 这里需要再判断一次,因为可能有其它线程已经创建实例
                if (instance == null) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

    public long getRamAddress() {
        return this.hashCode();
    }
}

        volatile关键词具备以下功能:
       1. 避免编译器将变量缓存在寄存器里 
       2. 避免编译器调整代码执行的顺序 

       使用volatile声明的变量可以强制屏蔽编译器和JIT的优化工作,能够防止双重检查锁的指令重排。

3.反射破坏单例

      正如在上篇文章饿汉式单例中所说的那样,懒汉式同样有反射的问题。我们采用跟懒汉式一样的方法常识防止破坏单例:

public class LazySingleton {

    private static volatile LazySingleton instance = null;

    // 让构造函数为private,这样该类就不会被实例化
    private LazySingleton() {
        //System.out.println("懒汉式单例初始化!");
        synchronized (LazySingleton.class) {
            if(instance != null){
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                // 这里需要再判断一次,因为可能有其它线程已经创建实例
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

    public long getRamAddress() {
        return this.hashCode();
    }
}

      再测试一下看看:

    public static void LazySingletonReflectionTest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<LazySingleton> clazz = LazySingleton.class;
        // 获取HungrySingLeton的默认构造函数
        Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 调用默认构造函数创建实例
        LazySingleton h1 = constructor.newInstance();
        LazySingleton h2 = constructor.newInstance();
        System.out.println(h1.getRamAddress());
        System.out.println(h2.getRamAddress());
    }

      运行结果:

      没用!因为在类加载的时候,懒汉式单例根本就没有对单例进行初始化,然后反射通过构造函数获取单例,获取的对象都是不同的,所以没法防止反射破坏。当然,如果是先通过getInstance获取单例,再反射,这种情况下就可以防止反射,如:

    public static void reflectionTest2() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        LazySingleton h0 = LazySingleton.getInstance();
        Class<LazySingleton> clazz = (Class<LazySingleton>) h0.getClass();
        // 获取HungrySingLeton的默认构造函数
        Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 调用默认构造函数创建实例
        LazySingleton h1 = constructor.newInstance();
        System.out.println(h0.getRamAddress());
        System.out.println(h1.getRamAddress());
    }

      懒汉式单例对这个问题没有好的解决办法。

4.优雅的单例实现——枚举

      单例模式存在线程安全、反射、序列化漏洞的问题,虽然可以想办法解决,但代码也比较臃肿了。换个角度,可以用枚举来实现,去规避这些问题。

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("这里实现自己的业务");
    }
}

4.1 枚举单例防止反射

       先测试一下枚举单例的反射:

    public static void enumSingletonRlectionTest() throws NoSuchMethodException,
            IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton instance1 = Singleton.INSTANCE;
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //java.lang.NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
        Singleton instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }

       结果抛出异常 ,没有构造函数Singleton.<init>():

       这并非防止了反射。枚举Enum是个抽象类,它有个构造函数:

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

       我们定义了枚举类单例,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器,测试方法改造下:

    public static void enumSingletonRlectionTest() throws NoSuchMethodException,
            IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton instance1 = Singleton.INSTANCE;
        Constructor<Singleton> declaredConstructor = Singleton.class.
                getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        //java.lang.NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
        Singleton instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }

       结果如下:

       Cannot reflectively create enum objects(无法以反射方式创建枚举对象),可以防止反射,因为反射在通过构造函数的newInstance方法创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。所以枚举类不能通过反射来创建对象

4.2 枚举单例避免序列化问题

       普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例,必须新增readResolve方法才能防止破坏。

       但是,枚举的反序列化并不是通过反射实现的。在序列化时,Java仅仅是将枚举对象的name属性输出到结果中,反序列化时则通过java.lang.Enum的valueOf(String name) 方法,根据名字查找内存中是否已经有该对象,若找到了就会直接使用它,如果不存在就会抛出异常。同时。编译器不允许任何对这种序列化机制的定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。这样一来,序列化的方式就无法创建新的对象了,也就不会发生由于反序列化导致的单例破坏问题。

        实际测试一下:

    public static void enumSingletonSerializable() {
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            File file = new File("d:\\Singleton.txt");
            // -----------序列化-------------
            // 创建输出流
            oos = new ObjectOutputStream(new FileOutputStream(file));
            //将单例对象写到文件中  序列化
            oos.writeObject(Singleton.INSTANCE);
            oos.flush();

            // -----------反序列化-------------
            // 从文件读取单例对象
            ois = new ObjectInputStream(new FileInputStream(file));
            // 反序列化得到对象singLeton
            Singleton singLeton= (Singleton)ois.readObject();
            System.out.println(singLeton == Singleton.INSTANCE); //false
            file.deleteOnExit();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(oos != null) {
                    oos.close();
                }
                if(ois != null) {
                    ois.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

     

       测试结果:

4.3 多线程安全

       当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承;同时,编译器所创建的类中,属性和方法也都是都是static类型的,因为static类型的属性会在类被加载之后被初始化。当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。所以,创建一个enum类型是线程安全的。

       基于前面的分析,单例类的线程安全问题,主要就是单例初始化过程中的线程安全问题。而由于枚举的以上特性,枚举实现的单例是天生线程安全的。所以用枚举实现的单例是最好的方式!

参考文章:

两种单例模式详解(内含懒汉式的双重校验锁详解)
为什么我墙裂建议大家使用枚举来实现单例

深度分析Java的枚举类型—-枚举的线程安全性及序列化问题

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

单例模式之懒汉式 的相关文章

  • 一篇文章,让你彻底搞懂单例设计模式

    今天在一群小哥哥的带领下 本程序媛终于学会了所有单例模式 非常感谢大哥哥 小哥哥 下文是我总结的单例模式的八种实现方式 如下所示 单例模式的简介 我们将一个类在当前进程中只有一个实例的这种模式 称之为 单例模式 那么Java代码如何实现一个
  • 设计模式—单例模式(饿汉式、懒汉式)

    目录 一 什么是单例模式 二 单例模式的类型 三 单例模式的公共特征 四 单例模式 饿汉式 五 单例模式 懒汉式 5 1 懒汉式实现方式一 有问题不提倡使用 5 2 懒汉式实现方式二 提倡使用 一 什么是单例模式 相信大家在面试过程中被提到
  • 单例模式详解,包括应用场景及懒汉式的线程安全问题

    什么是单例模式 所谓类的单例设计模式 就是采取一定的方法保证在整个的软件系统中 对某个类只能存在一个对象实例 并且该类只提供一个取得其对象实例的方法 如果我们要让类在一个虚拟机中只能产生一个对象 我们首先必须将类的构造器的访问权限设置为pr
  • Java实现设计模式之——单例模式

    目录 1 什么是单例模式 2 单例模式的实现 2 1 饿汉式单例模式 2 2 懒汉式单例模式 3 线程安全的单例模式 3 1 版本 1 3 2 版本 2 双重检测 3 3 版本 3 禁止指令重排 1 什么是单例模式 单例模式是 Java 中
  • 单例模式的4种写法

    单例模式是开发过程中常用的模式之一 首先了解下单例模式的四大原则 构造方法私有 以静态方法或枚举返回实例 确保实例只有一个 尤其是多线程环境 确保反射或反序列化时不会重新构建对象 饿汉模式 饿汉模式在类被初始化时就创建对象 以空间换时间 故
  • Java并发总结之Java内存模型

    本文主要参考 深入理解Java虚拟机 和 Java并发编程的艺术 对Java内存模型进行简单总结 一 CPU和缓存一致性 1 CPU高速缓存 为了解决CPU处理速度和内存处理速度不对等的问题 就是在CPU和内存之间增加高速缓存 当程序在运行
  • 单例模式(饿汉式单例 VS 懒汉式单例)

    所谓的单例模式就是保证某个类在程序中只有一个对象 一 如何控制只产生一个对象 1 构造方法私有化 保证对象的产生个数 创建类的对象 要通过构造方法产生对象 构造方法若是public权限 对于类的外部 可以随意创建对象 无法控制对象个数 构造
  • Java:多线程概述与创建方式

    文章目录 Java 多线程概述与创建方式 进程和线程 并发与并行 多线程的优势 线程的创建和启动 继承Thread类 start 和run 实现Runnable接口 实现Callable接口 创建方式的区别 Java 多线程概述与创建方式
  • C++工厂类和单例模式的结合使用

    单例模式 简单来说一个类只有一个实例且封装性好 这里用宏定义实现 animal singleton h pragma once include
  • C++多线程环境下的单例类对象创建

    使用C 无锁编程实现多线程下的单例模式 贺志国 2023 8 1 在多线程环境下创建一个类的单例对象 要比单线程环境下要复杂很多 下面介绍在多线程环境下实现单例模式的几种方法 一 尺寸较小的类单例对象创建 如果待创建的单例类Singleto
  • 单例模式的优缺点和使用场景

    单例模式 Singleton 也叫单子模式 是一种常用的软件设计模式 在应用这个模式时 单例对象的类必须保证只有一个实例存在 许多时候整个系统只需要拥有一个的全局对象 这样有利于我们协调系统整体的行为 比如在某个服务器程序中 该服务器的配置
  • Java并发编程之CyclicBarrier详解

    简介 栅栏类似于闭锁 它能阻塞一组线程直到某个事件的发生 栅栏与闭锁的关键区别在于 所有的线程必须同时到达栅栏位置 才能继续执行 闭锁用于等待事件 而栅栏用于等待其他线程 CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集
  • JavaScript设计模式(三)——单例模式、装饰器模式、适配器模式

    个人简介 个人主页 前端杂货铺 学习方向 主攻前端方向 正逐渐往全干发展 个人状态 研发工程师 现效力于中国工业软件事业 人生格言 积跬步至千里 积小流成江海 推荐学习 前端面试宝典 Vue2 Vue3 Vue2 3项目实战 Node js
  • Amdahl定律

    计算机科学中的一个重要定律 描述 系统中某部件由于采用某种方式使系统性能改进后 整个系统系能的提高与该方式的使用频率或占总的执行时间的比例有关 主要应用 改善 系统瓶颈 性能 Amdahl定律定义了加速比 加速比 采用改进措施后性能 未采用
  • Java中常见的设计模式

    目录 一 什么是设计模式 二 设计模式的类型 1 创建型模式 2 结构型模式 3 行为型模式 三 单例模式 1 代码示例 2 优点 3 缺点 4 使用场景 四 工厂模式 1 代码示例 2 优点 3 缺点 五 装饰模式 1 代码示例 2 优点
  • Java并发编程学习1-并发简介

    Java并发编程学习系列 Java并发编程学习 简介 线程的优势 发挥多处理器的强大能力 建模的简单性 异步事件的简化处理 响应更灵敏的用户界面 线程的风险 安全性问题 活跃性问题 性能问题 结语 简介 在早期的计算机中不包含操作系统 它们
  • Java设计模式-单例模式

    单例模式 在有些系统中 为了节省内存资源 保证数据内容的一致性 对某些类要求只能创建一个实例 这就是所谓的单例模式 单例模式的定义与特点 单例 Singleton 模式的定义 指一个类只有一个实例 且该类能自行创建这个实例的一种模式 例如
  • Java复习-25-单例设计模式

    单例设计模式 目的 使用场景 在实际开发下 会存在一种情况 某一种类在程序的整个生命周期中 只需要实例化一次就足够了 例如 系统数据类 由于操作系统只有一个 因此在程序初始化时该类只需要实例化一次 之后的系统数据更改都是在这一个实例化对象中
  • c3p0数据库连接池死锁问题和mysql重连,连接丢失

    c3p0参数解释 最常用配置 initialPoolSize 连接池初始化时创建的连接数 default 3 取值应在minPoolSize与maxPoolSize之间 c3p0 initialPoolSize 10 minPoolSize
  • Java 单例模式、工厂模式、代理模式

    文章目录 单例模式 概念 单例模式的类型 破坏单例模式 枚举实现单例模式 工厂模式 概述 简单工厂模式 工厂方法 抽象工厂 代理模式 Proxy 概述 静态代理 动态代理 单例模式 概念 单例模式指在内存中创建对象且仅创建一次的设计模式 在

随机推荐

  • DateUtil 工具类整理一些自己用到过得,有点逻辑处理的方法

    获取当前的年分 return public static int getSeasonId Calendar cal Calendar getInstance return cal get Calendar YEAR 获得本周一与当前日期相差
  • 硬件系统工程师宝典(19)-----原理图封装库,你画对了吗?

    各位同学大家好 欢迎继续做客电子工程学习圈 今天我们继续来讲这本书 硬件系统工程师宝典 上篇我们说到PCB制造过程中 不同的焊接工艺 布局元器件之间的距离要满足不同的要求 PCB走线时要从焊盘中心走线并且宽度小于焊盘宽度 以及丝印排布的建议
  • OpenAI 选择这家成立2年的8人团队做什么?

    当地时间 8 月 16 日 OpenAI 发布公告称收购了 Global Illumination 的团队 此笔交易更成为 OpenAI 自 2015 年成立以来首次对外收购 但并未公开交易涉及金额 据悉 该团队将参与 OpenAI 核心产
  • Win10竟然内置了一台虚拟机

    第一步 打开win10自带的虚拟机Hyper V 需要 win10系统 1 点击windows键 e键打开文件资源管理器 右击此电脑 gt 选择属性 gt 打开控制面板 2 选择控制面板主页 gt 选择程序与功能 3 点击启用或关闭Wind
  • 入住倒计时

    经过辛辛苦苦的三个月的时间 房子总算硬装结束 入住时间也排上了日程 查了查老黄历 西历2008 09 14即公历8月15 中国的中秋节 日子不错 是入住的黄道吉日 准备买点鞭炮小热闹一下 一来冲冲三个月来的劳累 烦躁的情绪 二来也算献给一期
  • springboot-配置类学习

    开发SpringBoot应用时经常涉及到配置文件 平时只是知道使用 ConfigurationProperties来注解类 或者使用 Value来获取配置值 通过 EnableConfigurationProperties来将配置类作为be
  • [正能量系列]失业的程序员(二)

    http blog csdn net shenyisyn article details 8634185 闹钟响 迷迷糊糊的我砸了一下开关 竟然把闹钟砸坏了 昨天接到学姐的电话 说是帮我介绍了一个钢管制造厂企业型宣传网站的业务 难度不大主要
  • 第六十六篇 三种常见的电路输出OC/OD和推挽

    1 OC输出 集电极开路输出 所谓OC就是open collector 字面理解就是collector极开路状态 既是集电极什么负载都不接 保持开路状态 在集电极开路输出OC结构电路中 类似上图 如果Q2断开的话 那么输出OC门的状态不可知
  • elementui中的tree组件相关操作集合

    目录 1 刷新指定节点 2 自定义过滤方法 3 新增子节点 4 编辑节点名 5 拖拽节点 6 某节点高亮 7 总结 8 组件整体的代码 1 刷新指定节点 node节点有一个loaded的属性 用来存储该节点是否展开过 刷新指定节点的思路 无
  • How to use appreciation and lie

    appreciation 1 理解 同情 体谅 I had no apprecation of the prombles you faced 我没有体谅到你当前所面临的困难 2 感谢 感谢 Please accept this gift i
  • PowerShell混淆相关

    相关技术文章 2016 05 26 利用机器学习检测恶意PowerShell https bbs pediy com thread 230002 htm 2018 11 09 FireEye 基于机器学习的模糊命令行检测 https www
  • git log 记录 patch

    如何打tag git tag a KPN FW v1 02 01 build01 m KPN FW v1 02 01 build01 git push origin tags git查看历史记录及修改内容 git whatchanged h
  • 《UNIX环境高级编程》学习笔记

    Unix环境高级编程 学习笔记 第一章 NUIX基础知识 1 5输入和输出 文件描述符通常是一个非负整数 用以标识一个特定进程正在访问的文件 运行一个新程序 所有shell会为其打开3个文件描述符 标准输入 输出 错误 不带缓冲的I O 标
  • HTML、CSS、JavaScript学习总结

    学习总结 HTML 网站开发的主要原则是 用标签元素HTML描述网页的内容结构 用CSS描述网页的排版布局 用JavaScript描述网页的事件处理 即鼠标或键盘在网页元素上的动作后的程序 HTML Hyper Text Mark up L
  • 爬取去哪儿酒店信息及评论

    爬取去哪儿酒店信息及评论 第一步 获取城市列表 import requests import json import codecs 去哪儿城市列表 url https touch qunar com h api hotel hotelcit
  • setPlainText

    QString toPlainText const void setPlainText const QString text setPlainText 顾名思义 是设置纯文本的 而setText 参数除了可以设置为纯文本之外 还可以设置为一
  • js随手笔记之一 存储之cookies

    概念 什么是cookie 全称 HTTP Cookie 最初是用于客户端存储回话信息的 cookie在性质上是绑定在特定的域名下 当设定一个cookie后再给创建它的域名发送请求时 都会包含这个cookie cookie的作用是什么 1 当
  • 数论整理之特殊数three:142857

    不重要的一篇文章 走马数 142857 1 142857 原数字 142857 2 285714 轮值 142857 3 428571 轮值 142857 4 571428 轮值 142857 5 714285 轮值 142857 6 85
  • vant中获取tab标签页的元素

    问题 vant框架中想要获取标签元素但又发现直接用 click和 change传id都无效 那应该怎么做呢 解决 在van tabs标签上顶一个点击事件 给van tab的name属性绑定一个唯一的值 然后将name作为参数传给该事件即可
  • 单例模式之懒汉式

    在上篇文章中 我们讲了单例模式中的饿汉式 今天接着来讲懒汉式 1 懒汉式单例模式的实现 public class LazySingleton private static LazySingleton instance null 让构造函数为