贯穿设计模式 -- 万字长文详解单例模式

🥳🥳🥳 茫茫人海千千万万,感谢这一刻你看到了我的文章,感谢观赏,大家好呀,我是最爱吃鱼罐头,大家可以叫鱼罐头呦~🥳🥳🥳

从今天开始,将开启一个专栏,【贯穿设计模式】,设计模式是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。为了能更好的设计出优雅的代码,为了能更好的提升自己的编程水准,为了能够更好的理解诸多技术的底层源码, 设计模式就是基石,万丈高楼平地起,一砖一瓦皆根基。 ✨✨欢迎订阅本专栏✨✨
🥺 本人不才,如果文章知识点有缺漏、错误的地方 🧐,也欢迎各位人才们评论批评指正!和大家一起学习,一起进步! 👀

❤️ 愿自己还有你在未来的日子,保持学习,保持进步,保持热爱,奔赴山海! ❤️

💬 最后,希望我的这篇文章能对你的有所帮助! 🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!

📃 前言回顾


🔥【贯穿设计模式】第八话·设计模式的七大原则之总结篇🔥

在之前的学习中,我们先学习了设计模式的七大原则,而设计模式就是通过这七个原则,来指导我们如何做一个好的设计,它是一套方法论,一种高内聚、低耦合的设计思想。当然我们可以在此基础上自由的发挥,甚至设计出符合自己系统的一套设计模式。

  • 开闭原则是总原则(要求我们对扩展开放,对修改关闭,即抽象架构,扩展实现)
  • 单⼀职责原则(要求我们现类要职责单一,即⼀个类和方法只做⼀件事 )
  • 依赖倒置原则(要求我们要面向接口编程细节,即依赖抽象,下层依赖上层)
  • 里氏替换原则(要求我们不要破坏继承体系,即子类可扩展替代父类)
  • 接口隔离原则(要求我们设计接口的时候要精简单一,即建立单⼀接口)
  • 迪米特法则(要求我们降低类之间的耦合,即对类最少知道,尽量降低耦合)
  • 合成复用原则(要求我们尽量使用合成/复用的方式,即尽量使用组合/聚合的方式)

也了解到设计模式还分为创建型模式、结构型模式、行为型模式。接下来我们就先学习创建型模式下的设计模式。

😛 单列模式

从本章开始,正式进入设计模式的学习。模式虽多,但难度不一,本章将要介绍的单例模式是结构最简单的设计模式,并且由它开始为大家逐一展现设计模式的魅力。

它是创建型模式,确保对象的唯一性。

注:有些内容或者图片来自refactoringguru.cn/design-patt...

😜 起因

我想在大家使用电脑的过程中,或多或少需要用到Windows任务管理器吧,无论是查看当前的应用的内存、硬盘占比,或者是关闭某个程序都可能用到Windows任务管理器吧。那你有没有注意到一个点呢?

你可以做这样的一个尝试:使用任意方式(在任务栏上右键弹出菜单上点击任务管理器,或者键盘上同时按住 esc + shift + ctrl 三个键)启动Windows任务管理器

在正常情况下,无论我们启动多少次的任务管理器,Windows系统它都始终只弹出一个任务管理器窗口,这也就是说,在一个Windows系统中,任务管理器窗口都是唯一的。而为什么会这样设计呢?

其实也很好理解,我们可以从这两方面入手分析:

  1. 如果弹出了多个任务管理器窗口,且这些窗口展示的内容完全一致,这样打开的就全部是重复对象,这势必会造成系统资源的浪费,内存的损耗,而且根本没有必要显示多个内容完全相同的窗口,我们只需要一个任务管理器窗口就好;
  2. 而如果弹出的多个任务管理器窗口内容不一致,问题就更加严重了,这意味着在某一瞬间应用的使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示idea应用的"CPU使用率"为10%,而在窗口B显示idea应用的"CPU使用率"为15%,到底哪个才是真实的呢?这就势必会给用户带来误解,更不可取。

由此可见,确保Windows任务管理器在系统中有且仅有一个非常重要。而在我们实际开发中,也会经常遇到这种类似情况,为了确保一个类中只需要有一个唯一实例,当这个唯一实例创建成功之后,无法再通过任何手段去重新创建当前实例对象,并且后续的所有操作都只能基于这个唯一实例。所以为了确保对象的唯一性,我们就可以通过单例模式来实现,这也就是单例模式的起因所在。

😝 简介

单例模式的主要定义:确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型设计模式。

单例模式可以说是整个设计中最简单的模式之⼀,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

😒 结构图

提示,在UML类图中成员前面的减号(-)表示私有(private)、加号(+)表示公有(public)以及井号(#)表示受保护的(protected)。

解释下:

我们将创建一个 Singleton 类(当然你可以为它取任意名字),这个唯一的Singleton类就是要实现为单例模式的类。该类图还展示了该类包含一个自己本身的一个私有静态成员变量、一个自身的私有的构造函数以及提供一个公有静态方法(供外界获取它的静态实例)。

注意:

单例的构造函数必须对外界隐藏,不允许外界私自创建实例,而获取单例对象的唯一方式只能是调用 获取实例静态公方法。

🌱 八种实现方式

对于单例模式的实现方式,不知道你真的了解多少呢?下面就让我为大家介绍下单例的实现方式:

  1. 饿汉式 -- 静态变量
  2. 饿汉式 -- 静态代码块
  3. 懒汉式 -- 线程不安全
  4. 懒汉式 -- 线程安全,同步方法
  5. 懒汉式 -- 线程安全,同步代码块
  6. 双重检测机制
  7. 静态内部类
  8. 枚举单例

单例模式的实现要点

单例模式要求类能够有返回实例对象(并且永远是同一个对象)和一个获得该实例对象的方法(必须是静态方法)。

单例模式的实现主要是通过以下三个步骤:

  1. 将类的构造方法定义为私有方法。这样其他类的代码就无法通过调用该类的构造方法来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例;
  2. 创建定义一个私有的类的静态实例对象;
  3. 提供一个公有的获取实例对象的静态方法。

🌲 饿汉式 -- 静态变量

实现步骤:

  1. 创建单例模式的类Singleton;
  2. 将类的构造函数私有化,避免外界使用构造函数创建实例;
  3. 定义一个私有的静态实例对象,并进行初始化;
  4. 提供一个公有的获取实例对象的静态方法getIntance()。

代码实现:

csharp 复制代码
package com.ygt.designPattern.singleton.demo1;
​
/**
 * 单例模式的实现
 *  饿汉式 -- 静态变量
 *  主要步骤有:
 *      1. 将类的构造函数私有化,避免外界使用构造函数创建实例;
 *      2. 定义一个私有的静态实例对象,并进行初始化;
 *      3. 提供一个公有的获取实例对象的静态方法getIntance()。
 *
 *  主要利用类加载到内存后,就实例化一个单例,JVM保证线程安全。
 *  简单实用,推荐实用!
 *  唯一缺点:不管用到与否,类装载时就完成实例化,导致内存资源的消耗。
 */
public class Singleton {
​
    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() { }
​
    // 2. 定义一个私有的静态实例对象,并进行初始化
    private static Singleton instance = new Singleton();
​
    // 3. 提供一个公有的获取实例对象的静态方法
    public static Singleton getInstance(){
        return instance;
    }
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo1;
​
/**
 * 单例模式的测试类
 * 饿汉式 -- 静态变量
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {
​
    public static void main(String[] args) {
        // 通过公有的方法获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
​
        // 获取两个实例的对象的引用地址和hashcode值
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());
​
        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));
    }
}

得到的结果:

kotlin 复制代码
com.ygt.designPattern.singleton.demo1.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo1.Singleton@1b6d3586
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

总结:

饿汉模式,比较常见的一种写法。在类加载的时候就对实例进行初始化,就实例化一个单例,JVM保证线程安全,没有线程安全问题;

而唯一缺点是不管用到与否,类装载时就完成实例化,而如果该实例从始至终都没被使用过,则会造成内存浪费。

是否推荐使用: 简单不复杂,易于实现,推荐实用~

🌴 饿汉式 -- 静态代码块

实现步骤:

  1. 创建单例模式的类Singleton;
  2. 将类的构造函数私有化,避免外界使用构造函数创建实例;
  3. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
  4. 定义一个静态代码块,并在这里初始化静态实例对象;
  5. 提供一个公有的获取实例对象的静态方法getIntance()。

代码实现:

csharp 复制代码
package com.ygt.designPattern.singleton.demo2;
​
/**
 * 单例模式的实现
 *  饿汉式 -- 静态代码块
 *  主要步骤有:
 *      1. 将类的构造函数私有化,避免外界使用构造函数创建实例;
 *      2. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
 *      3. 定义一个静态代码块,并在这里初始化静态实例对象;
 *      4. 提供一个公有的获取实例对象的静态方法getIntance()。
 *
 *  与饿汉式的另外一种静态变量,并无什么区别,只是可以在初始化前做一些操作。
 *  使用静态代码块的好处在于,可以在初始化本类对象前,做一些额外的操作。
 */
public class Singleton {
​
    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() { }
​
    // 2. 定义声明一个私有的静态实例对象,并没有初始化对象
    private static Singleton instance;
​
    // 3. 在静态代码块中进行初始化
    static {
        // 使用静态代码块的好处在于,可以在初始化本类对象前,做一些额外的操作。
        System.out.println("在饿汉式--静态代码块中,初始化前,做一些额外的操作");
        System.out.println("hello");
        instance = new Singleton();
    }
​
    // 4. 提供一个公有的获取实例对象的静态方法
    public static Singleton getInstance(){
        return instance;
    }
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo2;
​
/**
 * 单例模式的测试类
 * 饿汉式 -- 静态代码块
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {
​
    public static void main(String[] args) {
        // 通过公有的方法获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
​
        // 获取两个实例的对象的引用地址和hashcode值
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());
​
        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));
    }
}

得到的结果:

sql 复制代码
在饿汉式--静态代码块中,初始化前,做一些额外的操作
hello
com.ygt.designPattern.singleton.demo2.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo2.Singleton@1b6d3586
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

总结:

饿汉模式的另一种写法。与静态变量的方法并没有什么区别,也是在类加载的时候就对实例进行初始化,就实例化一个单例,JVM保证线程安全,没有线程安全问题,只是多了在初始化前后可以做一些操作;

而唯一缺点与静态变量一样,也是不管用到与否,类装载时就完成实例化,而如果该实例从始至终都没被使用过,则会造成内存浪费。

是否推荐使用: 简单不复杂,易于实现,推荐实用~

🌳 懒汉式 -- 线程不安全

上面都是介绍的饿汉式,两种方式的缺点都很明显,就是不管你是否用到该实例对象,都会在类加载时就实例化,造成内存的浪费。这样,如果系统中存在大量的单例对象时,那系统初始化就会导致大量的内存消耗。

那有什么更优方法呢?下面就来介绍另一种懒汉式,通过延迟加载的方式来初始化实例对象

实现步骤:

  1. 创建单例模式的类Singleton;
  2. 将类的构造函数私有化,避免外界使用构造函数创建实例;
  3. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
  4. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象。

代码实现:

csharp 复制代码
package com.ygt.designPattern.singleton.demo3;
​
/**
 * 单例模式的实现
 *  懒汉式 -- 线程不安全
 *  主要步骤有:
 *      1. 将类的构造函数私有化,避免外界使用构造函数创建实例;
 *      2. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
 *      3. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象。
 *
 *  虽然达到了延迟加载初始化的目的,但却带来线程不安全的问题。
 */
public class Singleton {
​
    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() { }
​
    // 2. 定义声明一个私有的静态实例对象,并没有初始化对象
    private static Singleton instance;
​
    // 3. 提供一个公有的获取实例对象的静态方法,在获取时来初始化实例对象
    public static Singleton getInstance(){
        // 懒汉式是在得到本类对象时,才会创建该对象
​
        // 如果不进行判断,每次都会创建一个新的对象
        // 所以加一个判断,判断 instance 是否为 null, 如果为null,就需要创建新的对象
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo3;
​
/**
 * 单例模式的测试类
 * 懒汉式 -- 线程不安全
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {
​
    public static void main(String[] args) {
        // 通过公有的方法获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
​
        // 获取两个实例的对象的引用地址和hashcode值
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());
​
        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));
    }
}

得到的结果:

kotlin 复制代码
com.ygt.designPattern.singleton.demo3.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo3.Singleton@1b6d3586
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

多线程带来的问题演示:

修改Singleton类

csharp 复制代码
package com.ygt.designPattern.singleton.demo3;

public class Singleton {

	// 这里省略上面代码,一样的,可以看后面的图片
    public static Singleton getInstance(){
        if(instance == null) {
            // 在这里加个睡眠1ms,这样更能验证线程安全问题
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton();
        }
        return instance;
    }
}

修改测试类,并重新测试

arduino 复制代码
package com.ygt.designPattern.singleton.demo3;

/**
 * 单例模式的测试类
 * 懒汉式 -- 线程不安全
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
       
        // 之前的代码注释掉

        // 启动10个线程测试
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Singleton instance = Singleton.getInstance();
                // 查看每个对象的hashcode
                System.out.println("instance.hashCode() = " + instance.hashCode());
            }).start();
        }
    }
}

查看结果

scss 复制代码
instance.hashCode() = 919115337
instance.hashCode() = 1849980617
instance.hashCode() = 270921682
instance.hashCode() = 1236783213
instance.hashCode() = 270921682
instance.hashCode() = 587167875
instance.hashCode() = 919115337
instance.hashCode() = 2137033978
instance.hashCode() = 932094492
instance.hashCode() = 1902102109

我们可以看到,有些的hashcode并不相同,这也代表了,确实产生了线程安全问题。

总结:

懒汉式,第一种实现方式,在调用getInstance()方法获取时,Singleton类的对象的时候才开始初始化Singleton类的对象,这样就实现了懒加载的效果,但是只能在单线程下使用;而如果在多线程情况下,就会出现线程安全问题。

如一个线程进入了if(instance == null )判断语句,还未来得及往下执行初始化操作,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可使用这种方式。

是否推荐使用: 简单不复杂,易于实现,但并不支持多线程,会出现线程安全问题,并不推荐。

🌵 懒汉式 -- 线程安全

上面介绍的懒汉式的方式,会出现线程安全问题,并不推荐使用,那如何保证线程安全问题,第一个想到的就是加锁,那接下来先了解下第一种锁方式,在方法上加同步锁synchronized。

实现步骤:

  1. 创建单例模式的类Singleton;
  2. 将类的构造函数私有化,避免外界使用构造函数创建实例;
  3. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
  4. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象,并在方法上加上同步锁关键字synchronized。

代码实现:

csharp 复制代码
package com.ygt.designPattern.singleton.demo4;

/**
 * 单例模式的实现
 *  懒汉式 -- 线程安全
 *  主要步骤有:
 *      1. 将类的构造函数私有化,避免外界使用构造函数创建实例;
 *      2. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
 *      3. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象,并在方法上加上同步锁关键字synchronized。
 *
 *  虽然达到了线程安全的目的,但是在方法上加同步锁后,导致效率降低。
 */
public class Singleton {

    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() { }

    // 2. 定义声明一个私有的静态实例对象,并没有初始化对象
    private static Singleton instance;

    // 3. 提供一个公有的获取实例对象的静态方法,在获取时来初始化实例对象,并在方法上加上同步锁关键字synchronized。
    public static synchronized Singleton getInstance(){
        // 懒汉式是在得到本类对象时,才会创建该对象

        // 如果不进行判断,每次都会创建一个新的对象
        // 所以加一个判断,判断 instance 是否为 null, 如果为null,就需要创建新的对象
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo4;

/**
 * 单例模式的测试类
 * 懒汉式 -- 线程安全
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
        // 通过公有的方法获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();

        // 获取两个实例的对象的引用地址和hashcode值
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());

        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));

    }
}

得到的结果:

kotlin 复制代码
com.ygt.designPattern.singleton.demo4.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo4.Singleton@1b6d3586
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

接下来看测试下看看是否解决了多线程问题:

修改Singleton类

java 复制代码
package com.ygt.designPattern.singleton.demo4;

public class Singleton {

	// 这里省略上面代码,一样的,可以看后面的图片
    public static synchronized Singleton getInstance(){
        if(instance == null) {
            // 在这里加个睡眠1ms,这样更能验证线程安全问题
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton();
        }
        return instance;
    }
}

修改测试类,并重新测试

arduino 复制代码
package com.ygt.designPattern.singleton.demo4;

/**
 * 单例模式的测试类
 * 懒汉式 -- 线程安全
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
       
        // 之前的代码注释掉

        // 启动10个线程测试
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Singleton instance = Singleton.getInstance();
                // 查看每个对象的hashcode
                System.out.println("instance.hashCode() = " + instance.hashCode());
            }).start();
        }
    }
}

查看结果

scss 复制代码
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682
instance.hashCode() = 270921682

我们可以看到,得到的hashcode的值都一致,这也代表使用同步锁可以解决多线程安全问题。

总结:

懒汉式,第二种实现方式,实现了懒加载的效果,同时也解决了多线程安全问题,但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。

但是我们可以看出,每次获取实例都要进行同步(加锁),因此效率较低,并且可能很多同步都是没必要的。

是否推荐使用: 简单不复杂,易于实现,虽然解决了多线程问题,但是加锁造成了效率的降低,不推荐使用。

🌾 懒汉式 -- 线程安全,同步代码块

上面介绍的第二种懒汉式的方式,虽然解决线程安全问题,但是效率比较低,也并不推荐使用,那有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢。

其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了,后面的想获得该类实例,直接return就行。

实现步骤:

  1. 创建单例模式的类Singleton;
  2. 将类的构造函数私有化,避免外界使用构造函数创建实例;
  3. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
  4. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象,并在初始化对象时加上同步代码块。

代码实现:

csharp 复制代码
package com.ygt.designPattern.singleton.demo5;

/**
 * 单例模式的实现
 *  懒汉式 -- 线程安全,同步代码块
 *  主要步骤有:
 *      1. 将类的构造函数私有化,避免外界使用构造函数创建实例;
 *      2. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
 *      3. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象,并在初始化对象时加上同步代码块。
 *
 *  虽然修改了同步锁为同步代码块,但是它依然会产生多线程安全问题,这是为什么呢?我们就一起验证下吧!
 */
public class Singleton {

    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() { }

    // 2. 定义声明一个私有的静态实例对象,并没有初始化对象
    private static Singleton instance;

    // 3. 提供一个公有的获取实例对象的静态方法,在获取时来初始化实例对象,并在初始化对象时加上同步代码块。
    public static Singleton getInstance(){
        // 懒汉式是在得到本类对象时,才会创建该对象

        // 如果不进行判断,每次都会创建一个新的对象
        // 所以加一个判断,判断 instance 是否为 null, 如果为null,就需要创建新的对象
        if(instance == null) {

            // 在此加上同步代码块
            synchronized(Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo5;

/**
 * 单例模式的测试类
 * 懒汉式 -- 线程安全,同步代码块
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
        // 通过公有的方法获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();

        // 获取两个实例的对象的引用地址和hashcode值
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());

        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));

    }
}

得到的结果:

kotlin 复制代码
com.ygt.designPattern.singleton.demo5.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo5.Singleton@1b6d3586
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

接下来看测试下看看是否解决了多线程问题:

修改Singleton类

typescript 复制代码
package com.ygt.designPattern.singleton.demo5;

public class Singleton {

	// 这里省略上面代码,一样的,可以看后面的图片
    public static synchronized Singleton getInstance(){
        if(instance == null) {
           // 在此加上同步代码块
            synchronized(Singleton.class){
                // 在这里加个睡眠1ms,这样更能验证线程安全问题
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Singleton();
            }
        }
        return instance;
    }
}

修改测试类,并重新测试

arduino 复制代码
package com.ygt.designPattern.singleton.demo5;

/**
 * 单例模式的测试类
 * 懒汉式 -- 线程安全,同步代码块
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
       
        // 之前的代码注释掉

        // 启动10个线程测试
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Singleton instance = Singleton.getInstance();
                // 查看每个对象的hashcode
                System.out.println("instance.hashCode() = " + instance.hashCode());
            }).start();
        }
    }
}

查看结果

scss 复制代码
instance.hashCode() = 1306755473
instance.hashCode() = 270921682
instance.hashCode() = 932094492
instance.hashCode() = 1902102109
instance.hashCode() = 2016912823
instance.hashCode() = 919115337
instance.hashCode() = 1482438336
instance.hashCode() = 252442137
instance.hashCode() = 1849980617
instance.hashCode() = 2113902944

我们可以看到,有些的hashcode并不相同,这也代表了,确实产生了线程安全问题,但是为什么呢?

其实也好理解,多个线程进入if(instance == null) 判断语句后,接下来遇到同步代码块后线程阻塞,但是阻塞完成后,直接二话不说就重新初始化了,所以这就是创建多个实例的原因所在。

总结:

懒汉式,第三种实现方式,实现了懒加载的效果,虽然解决了在方法上加同步锁导致效率低,但是此时的代码也会造成多线程安全问题,需要进一步的改进。

是否推荐使用: 简单不复杂,易于实现,虽然解决效率低下问题,但会出现线程安全问题,并不推荐。

🌿 双重检测机制

上面介绍的第三种懒汉式的方式,虽然解决效率低下的问题,但是又导致线程安全问题,所以并不推荐使用,那有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢。

其实就是在进入同步代码后,再进一步判断是否为第一次创建实例对象,所以就可以避免重复创建新对象的情况。

实现步骤:

  1. 创建单例模式的类Singleton;
  2. 将类的构造函数私有化,避免外界使用构造函数创建实例;
  3. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
  4. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象,并在初始化对象时加上同步代码块,同时加入双重检查代码。

代码实现:

csharp 复制代码
package com.ygt.designPattern.singleton.demo6;

/**
 * 单例模式的实现
 *  双重检查机制
 *  主要步骤有:
 *      1. 将类的构造函数私有化,避免外界使用构造函数创建实例;
 *      2. 定义声明一个私有的静态实例对象,在这里不进行初始化操作;
 *      3. 提供一个公有的获取实例对象的静态方法getIntance(),在获取时来初始化实例对象,并在初始化对象时加上同步代码块,同时加入双重检查代码。
 *
 *  再前面的一种实现形式下,加入第二段判空操作,可以完美实现单例模式。
 */
public class Singleton {

    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() { }

    // 2. 定义声明一个私有的静态实例对象,并没有初始化对象
    // 可能在jvm编译过程中,发生指令重排序问题,导致空指针异常,需要加 volatile关键字
    private static volatile Singleton instance;

    // 3. 提供一个公有的获取实例对象的静态方法,在获取时来初始化实例对象,并在初始化对象时加上同步代码块。
    public static Singleton getInstance(){
        // 懒汉式是在得到本类对象时,才会创建该对象

        // 如果不进行判断,每次都会创建一个新的对象
        // 第一次判断,判断 instance 是否为null, 如果为null,就需要创建新的对象,如果不为null,就不需要抢占锁,直接返回对象
        if(instance == null) {

            // 在此加上同步代码块
            synchronized(Singleton.class){
                // 这里加上第二次判断,避免重复创建新对象。
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo6;

/**
 * 单例模式的测试类
 * 双重检查机制
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
        // 通过公有的方法获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();

        // 获取两个实例的对象的引用地址和hashcode值
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());

        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));
    }
}

得到的结果:

kotlin 复制代码
com.ygt.designPattern.singleton.demo6.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo6.Singleton@1b6d3586
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

这里就不再测试线程安全问题了,因为解决了,但是可能会出现一个新的问题,可能在jvm编译过程中,发生指令重排序问题,导致空指针异常,需要加volatile关键字,如果需要了解指令重排序,指令重排序相关文章我也写过或者大家可以去网上查找下资料即可,大致描述下:

在new对象时即 instance = new Singleton(),正常它会发生这三个步骤:

  1. 在JVM的堆中申请一块内存空间
  2. 对象进行初始化操作
  3. 将堆中的内存空间的引用地址赋值给一个引用变量instance。

但是如果没有加上volatile关键字时,可能导致出现的步骤为 1 -> 3 -> 2

这时候就会出现空指针异常啦,所以我们得加在instance变量加上关键字 volatile,确保在初始化变量时不会发送指令重排序问题。

总结:

懒汉式,第四种实现方式,既实现了懒加载的效果,也解决线程安全问题和效率低下问题。

是否推荐使用: 推荐使用,完美的单例模式写法之一。

☘️ 静态内部类

上面的双重检测机制方式,无论怎样都是会用到锁,而锁无论怎样都是会程序性能造成一定影响的,那有没有更好的方法呢?有滴,下面就来介绍下:

这是第二种完美的单例模式的写法,用jvm的类的加载机制保证实例对象的唯一以及线程安全问题。

实现步骤:

  1. 创建单例模式的类Singleton;
  2. 将类的构造函数私有化,避免外界使用构造函数创建实例;
  3. 定义创建一个静态内部类,并创建初始化类的静态实例对象;
  4. 提供一个公有的获取实例对象的静态方法getIntance()。

代码实现:

csharp 复制代码
package com.ygt.designPattern.singleton.demo7;

/**
 * 单例模式的实现
 *  静态内部类
 *  主要步骤有:
 *      1. 将类的构造函数私有化,避免外界使用构造函数创建实例;
 *      2. 定义创建一个静态内部类,并创建初始化类的静态实例对象;
 *      3. 提供一个公有的获取实例对象的静态方法getIntance()。
 *
 *  完美的单例模式之一,用类的加载机制确保对象的唯一性。
 */
public class Singleton {

    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() { }

    // 2. 定义创建一个静态内部类,并创建初始化类的静态实例对象;
    private static class SingletonHolder{
        // 创建初始化静态类对象
        private static final Singleton INSTANCE = new Singleton();
    }

    // 3. 提供一个公有的获取实例对象的静态方法。
    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo7;

/**
 * 单例模式的测试类
 * 静态内部类
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
        // 通过公有的方法获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();

        // 获取两个实例的对象的引用地址和hashcode值
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());

        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));
    }
}

得到的结果:

kotlin 复制代码
com.ygt.designPattern.singleton.demo7.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo7.Singleton@1b6d3586
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

这里也不多演示线程安全的问题了,因为它已经解决了这个问题,接下来来稍微解释下为啥能确保对象的唯一性:

main() 方法启动后,外部类Singleton被装载时,此时静态内部类SingletonHolder不会被转载,而当需要用的时候即调用了 Singleton.getInstance() ,才会去装载静态内部类,而类的静态属性只会在第一次加载类的时候初始化,这里是利用jvm的类装载机制来保证初始化 INSTANCE 时只有一个线程。

总结:

懒汉式,第五种实现方式,实现了懒加载的效果,也更好的解决线程安全问题,以及使用关键字synchronized带来的性能问题。

是否推荐使用: 推荐使用,完美的单例模式写法之一。

🍀 枚举单例

但是上面的这些所有方法真的完美吗,其实不然,下面有个更完美的实现方式,更为简单。

这是第三种完美的单例模式的写法。

实现步骤:

  1. 创建单例模式的枚举类Singleton;
  2. 创建一个变量INSTANCE。

代码实现:

markdown 复制代码
package com.ygt.designPattern.singleton.demo8;

/**
 * 单例模式的测试类
 * 枚举单例
 *  主要步骤有:
 *      1. 创建单例模式的枚举类Singleton;
 *      2. 创建一个变量INSTANCE。
 *
 *  完美的单例模式。
 */
public enum Singleton {
    /**
     * 直接定义变量INSTANCE。
     */
    INSTANCE;
}

创建测试类并测试创建的对象是否唯一:

csharp 复制代码
package com.ygt.designPattern.singleton.demo8;


/**
 * 单例模式的测试类
 * 枚举单例
 *  获取类的hashcode,使用 == 判断是否为同一引用
 */
public class SingletonTest {

    public static void main(String[] args) {
        // 直接获取 INSTANCE
        Singleton instance = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;

        // 获取两个实例的对象hashcode值
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());

        // 判断是否相同
        System.out.println("两个类是否相同:" + (instance == instance2));
    }
}

得到的结果:

scss 复制代码
instance.hashCode() = 460141958
instance2.hashCode() = 460141958
两个类是否相同:true

这里也不多演示线程安全的问题了,枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。那其他单例模式怎么被破坏呢?后面我们再来说说

总结:

它是饿汉式的形式,很简洁的一种实现方式,提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。

是否推荐使用: 推荐使用,完美的单例模式写法之一。

🍒 破坏单例模式

何为破坏单例模式呢?就是使单例模式的类 Singleton 创建多个对象,造成对象的不唯一性,就是破坏了单例模式。

总共有两种破坏方式:序列化和反射。

注:除了枚举方式外。

🍓 序列化

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。但是反序列化后的对象会重新分配内存,即重新创建,这也就会破坏了单例模式。

接下来以demo7的静态内部类的单例模式来测试下序列化破坏单例模式。

实现步骤:

  1. 将demo7的Singleton类实现Serializable接口;

  2. 向文件写入对象数据;

  3. 从文件中读取对象数据;

  4. 多次读取,查看得到的对象是否相同

  5. Singleton类实现Serializable接口:

java 复制代码
package com.ygt.designPattern.singleton.demo7;

import java.io.Serializable;

public class Singleton implements Serializable {

   // 代码省略
}
  1. 创建一个测试类,并创建方法实现向文件写入对象数据和从文件中读取对象数据:
java 复制代码
package com.ygt.designPattern.singleton.demo7;

import java.io.*;

/**
 * 测试序列化破坏单例模式
 * 主要步骤:
 *  1. 向文件写入对象数据;
 *  2. 从文件中读取对象数据;
 *  3. 多次读取,查看得到的对象是否相同
 */
public class SerializationTest {

    // 定义一个全局的存储文件的地址路径 当前路径吧
    private static final String FILE_PATH = "D:\testSpace\design_pattern\src\main\java\com\ygt\designPattern\singleton\demo7\test.text";

    // 1. 向文件写入对象数据;作为演示,这里直接抛出异常即可。
    private static void writeObject() throws Exception {
        //获取Singleton类的对象
        Singleton instance = Singleton.getInstance();

        //创建对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));

        // 将instance对象写出到文件中
        oos.writeObject(instance);
        
        // 释放资源
        oos.close();
    }

    // 2. 从文件中读取对象数据;作为演示,这里直接抛出异常即可。
    private static Singleton readObject() throws Exception {
        // 创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));

        // 读取Singleton对象
        Singleton instance = (Singleton) ois.readObject();

        // 释放资源
        ois.close();
        
        // 返回对象
        return instance;
    }
}
  1. 创建main方法, 多次读取,查看得到的对象是否相同:
scss 复制代码
// 3. 创建main方法, 多次读取,查看得到的对象是否相同
public static void main(String[] args) throws Exception {
    // 1. 先写入对象数据到文件
    writeObject();

    // 2. 读取对象
    Singleton instance = readObject();
    Singleton instance2 = readObject();

    // 获取两个实例的对象的引用地址和hashcode值
    System.out.println(instance);
    System.out.println(instance2);
    System.out.println("instance.hashCode() = " + instance.hashCode());
    System.out.println("instance2.hashCode() = " + instance2.hashCode());

    // 判断是否相同
    System.out.println("两个类是否相同:" + (instance == instance2));
}
  1. 得到的结果:
kotlin 复制代码
com.ygt.designPattern.singleton.demo7.Singleton@312b1dae
com.ygt.designPattern.singleton.demo7.Singleton@7530d0a
instance.hashCode() = 824909230
instance2.hashCode() = 122883338
两个类是否相同:false

可以看到,貌似完美的静态内部类,这样就被破解了,非常难受呀,这也表示了序列化和反序列化已经破坏了单例设计模式。如果各位有兴趣,也可以看看枚举的单例模式是不是自动就解决了这个问题呢?

🍅 序列化解决方案

我们可以看出反序列化后的对象是多次实例化了,违背了单例模式,那我们如何保证在序列化的情况下也能够实现单例模式呢﹖其实很简单,只需要增加readResolve()方法即可。

在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

修改Singelton类,添加readResolve():

java 复制代码
package com.ygt.designPattern.singleton.demo7;

import java.io.Serializable;

public class Singleton implements Serializable {

   // 其他代码省略

    /**
     * 解决序列化反序列化破解单例模式
     */
    private Object readResolve() {
        // 这里打印下判断是否真的调用了这里呢?
        System.out.println("你好呀,召唤师!");
        return SingletonHolder.INSTANCE;
    }
}

重新测试,得到的结果:

kotlin 复制代码
你好呀,召唤师!
你好呀,召唤师!
com.ygt.designPattern.singleton.demo7.Singleton@3764951d
com.ygt.designPattern.singleton.demo7.Singleton@3764951d
instance.hashCode() = 929338653
instance2.hashCode() = 929338653
两个类是否相同:true

可以看到,确实解决序列化的问题,而且也进入到 readResolve() 方法中,那具体是为什么?这里粗略的跟进下ObjectInputStream源码吧,让大家能够稍微了解下:

我们能够知道在反序列化过程中,得到的对象是在 ois.readObject() 这一步返回的,所以如果创建的对象不一致也是这里造成的,所以一切根源从这里面进去查看即可。

备注:如果想更详细,可以自行进入源码查看呦,自行查看食用更佳。

  1. 类SerializationTest中的readObject()方法:
ini 复制代码
Singleton instance = (Singleton) ois.readObject();
  1. 进入到该类的源码查看:
java 复制代码
// 首先进入到这个地方无参的方法,里面又跳转到重写的带参方法中去
public final Object readObject()
    throws IOException, ClassNotFoundException {
    return readObject(Object.class);
}

// 再进入到以下方法
private final Object readObject(Class<?> type)
    throws IOException, ClassNotFoundException
{
    // 这里可以无需在意这些代码,以下是一些判断
     // ...省略代码

    
    try {
        // 重点来了,这里是读取的方法,最后返回的Object就是我们要的对象。
        Object obj = readObject0(type, false);
         // ...省略代码
        
        // 可以看到最后返回的对象是obj
        return obj;
    } finally {
         // ...省略代码
    }
}
  1. 继续查看readObject0()方法:
typescript 复制代码
// 进入到以下方法
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
     // ...省略代码

    byte tc;
    while ((tc = bin.peekByte()) == TC_RESET) {
        bin.readByte();
        handleReset();
    }

    depth++;
    totalObjectRefs++;
    try {
        // switch 判断我们的对象是什么类型,再去调用不同的方法,我们是Object对象,所以直接找Object相关即可。
        switch (tc) {
            case TC_NULL:
                return readNull();

             // ...省略代码

            case TC_OBJECT:
                if (type == String.class) {
                    throw new ClassCastException("Cannot cast an object to java.lang.String");
                }
                // 来到这里,可以发现又会调用了readOrdinaryObject这个方法,接下来再去这里查看吧
                return checkResolve(readOrdinaryObject(unshared));

             // ...省略代码
        }
    } finally {
         // ...省略代码
    }
}
  1. 继续查看readOrdinaryObject()方法:
scss 复制代码
private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
   // ...省略代码

    Object obj;
    try {
        // 可以看到这里,直接相当于是实例化的操作,里面判断构造函数是否为空,无参就实例化。
        // 那这里就初始化了,那怎么使对象一致的呢?继续往下找
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }

    // ...省略代码

    // 看到这末尾这里,主要的思想就是多了hasReadResolveMethod()这一步,
    // 这一步就是判断我们的类是否有包含readResolve()方法,有就走我们的自己的方法,创建同一个对象。
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        // 这里就会去反射调用 Singleton的 readResolve() 方法,将返回的对象赋值到rep
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            // Filter the replacement object
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            // 这里就通过将 readResolve() 得到rep 重新赋值到obj中
            // 也就是通过这里将Singleton的自己创建对象重新赋值过来。
            handles.setObject(passHandle, obj = rep);
        }
    }
	
    // 最后返回
    return obj;
}

我们可以看到,这样的反序列化对象的过程实际上初始化了两次对象,只不过最后返回的是Singleton的自己创建对象。

🍠 反射

上面的实现过程中,可以发现单例模式的构造方法除了加上 private关键字,没有做任何处理。我们要知道反射是非常暴力的,不管你是否是私有,怎样我都能获取到。所以如果使用反射来调用其构造方法,再调用 getInstance()方法,是否会破坏了单例模式呢?

接下来以demo6的双重检测的单例模式来测试下反射破坏单例模式。

实现步骤:

  1. 获取Singleton的字节码对象;
  2. 通过反射获取私有的无参构造函数;
  3. 暴力反射,强制访问;
  4. 创建Singleton对象;
  5. 判断多个对象是否相同。

代码实现测试类:

kotlin 复制代码
package com.ygt.designPattern.singleton.demo6;

import java.lang.reflect.Constructor;

/**
 * 测试反射是否能够破坏单例模式呢
 * 主要步骤:
 *   1. 获取Singleton的字节码对象;
 *   2. 通过反射获取私有的无参构造函数;
 *   3. 暴力反射,强制访问;
 *   4. 创建Singleton对象;
 *   5. 判断多个对象是否相同。
 */
public class ReflectTest {
    public static void main(String[] args) throws Exception {
        // 1. 获取Singleton的字节码对象
        Class clazz = Singleton.class;

        // 2. 通过反射获取私有的无参构造函数
        Constructor constructor = clazz.getDeclaredConstructor();

        // 3. 暴力反射,强制访问
        constructor.setAccessible(true);

        // 4. 创建Singleton对象
        Singleton instance = (Singleton) constructor.newInstance();
        Singleton instance2 = (Singleton) constructor.newInstance();

        // 5. 判断多个对象是否相同
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println("instance.hashCode() = " + instance.hashCode());
        System.out.println("instance2.hashCode() = " + instance2.hashCode());
        System.out.println("两个对象是否相同:" + (instance == instance2));
    }
}

得到的结果:

kotlin 复制代码
com.ygt.designPattern.singleton.demo6.Singleton@1b6d3586
com.ygt.designPattern.singleton.demo6.Singleton@4554617c
instance.hashCode() = 460141958
instance2.hashCode() = 1163157884
两个对象是否相同:false

很显然,反射依然能够破坏单例模式。

🥩 反射解决方案

我们可以看出反射创建出来的对象是多次初始化了,利用空参构造函数来多次创建初始化,违背了单例模式,那我们如何避免别人使用暴力反射创建多个实例呢?

在空参构造函数中,进行判断即可。

修改Singelton类,添加一个全局变量,并在构造函数中进行判断:

java 复制代码
package com.ygt.designPattern.singleton.demo6;

public class Singleton {

    // 创建一个变量来判断重复创建实例
    private static Boolean repeatFlag = false;

    // 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
    private Singleton() {
        // 防止反射破坏单例模式
        if(repeatFlag) {
            throw new RuntimeException("不允许反射创建对象");
        }

        repeatFlag = true;
    }

    // 其他代码省略
   
}

重新测试,得到的结果:

php 复制代码
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.ygt.designPattern.singleton.demo6.ReflectTest.main(ReflectTest.java:27)
Caused by: java.lang.RuntimeException: 不允许反射创建对象
	at com.ygt.designPattern.singleton.demo6.Singleton.<init>(Singleton.java:22)
	... 5 more

可以看到,确实解决反射利用构造函数创建多个实例的问题,当通过反射方式调用构造方法进行创建创建时,直接抛异常。

🍟 单例模式小总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。

在我们的八种实现方式中,一般情况下,我们并不建议使用第三种、第四种和第五种的懒汉式方式。通常建议使用饿汉式的实现方式即可,简单易写,而且没有线程安全问题。如果需要明确实现懒加载的效果时,才会使用到双重检测或者静态内部类;如果需要涉及到序列化对象时,可以使用静态内部类的解决反序列的方法,当然最后也可以使用枚举的方式。

单例模式有以下特点:

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

单例模式的优点:

  • 由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统运行效率。

单例模式的缺点:

  • 因为系统中只有一个实例,导致了单例类的职责过重,违背了"单一职责原则",同时不利于扩展。

单例模式的适用场景:

虽然单例模式并不复杂但是使用面却比较广,在现实生活中的应用非常广泛,例如有:

  1. Windows系统的任务管理器;
  2. Windows系统的回收站;
  3. 操作系统的文件系统,一个操作系统只能有一个文件系统;
  4. 数据库连接池的设计与实现,指数据库的连接池不会反复创建;
  5. spring中⼀个单例模式bean的生成和使用;
  6. Java-Web中,一个Servlet类只有一个实例;
  7. 在我们平常的代码中需要设置全局的的⼀些属性保存。

应用场景通常可以分为以下几个场景:

  1. 需要频繁访问数据库或文件的对象;
  2. 需要在整个项目系统中生成一个共享访问点或者共享数据;
  3. 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用;
  4. 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少GC;
  5. 在系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则会导致系统完全乱套。

🌸 完结

相信各位看官看到这里大致都对设计模式中的创建型模式的其中一个模式有了了解吧,单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。通常我们会有八种实现方式,建议使用饿汉式的实现方式即可,简单易写,而且没有线程安全问题。当然项目不同,实现方式不同。单例模式,确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

学好设计模式,让你感受一些机械化代码之外的程序设计魅力,也可以让你理解各个框架底层的实现原理。最后,祝大家跟自己能在程序员这条越走越远呀,祝大家人均架构师,我也在努力。 明天我们将要学习的是创建型模式的简单工厂模式 (简单工厂模式并不是我们23种经典设计模式,但通常将它作为学习其他工厂模式的基础)💪💪💪

文章的最后来个小小的思维导图:

🧐 本人不才,如有什么缺漏、错误的地方,也欢迎各位人才们评论批评指正!🤞🤞🤞

🤩 当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才们给个点赞、收藏下吧,非常感谢!🤗🤗🤗

🥂 虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!✨✨✨

💟 感谢各位看到这里!愿你韶华不负,青春无悔!让我们一起加油吧! 🌼🌼🌼

💖 学到这里,今天的世界打烊了,晚安!🌙🌙🌙

相关推荐
暗黑起源喵5 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
齐 飞35 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod1 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man2 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*2 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu2 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s2 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子2 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王2 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构