一起来学设计模式之单例模式

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 前言目前正在出一个设计模式专题系列教程, 篇幅会较多, 喜欢的话,给个关注❤️ ~本节给大家讲一下设计模式中的单例模式~本专题的所有案例代码主要以Java语言为主, 好了, 废话不多说直接开整吧~单例模式上节带大家看了一下设计模式的基本概念,本节带大家一起实现一下设计模式中的单例模式。单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点。单例模式适用于需要确保系统中只有一个实例,并且需要提供一个全局访问点的场景,比如线程池、日志系统、配置文件管理器等。

前言

目前正在出一个设计模式专题系列教程, 篇幅会较多, 喜欢的话,给个关注❤️ ~

本节给大家讲一下设计模式中的单例模式~

本专题的所有案例代码主要以Java语言为主, 好了, 废话不多说直接开整吧~

单例模式

上节带大家看了一下设计模式的基本概念,本节带大家一起实现一下设计模式中的单例模式

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点

单例模式适用于需要确保系统中只有一个实例,并且需要提供一个全局访问点的场景,比如线程池、日志系统、配置文件管理器等。

下面看一个简单的例子:

懒汉式(线程不安全)

public class Singleton01 {
    private static Singleton01 instance;
    private Singleton01() {
        // 构造函数私有化,确保只能通过getInstance()方法获取实例
    }
    public static Singleton01 getInstance() {
        if (instance == null) {
            System.out.println("instance = null");
            instance = new Singleton01();
        }
        return instance;
    }
    public static void main(String[] args) {
        Singleton01 singleton01 = Singleton01.getInstance();
        System.out.println(singleton01.hashCode());
        Singleton01 singleton02 = Singleton01.getInstance();
        System.out.println(singleton02.hashCode());
        System.out.println(singleton01.equals(singleton02));
    }
}

运行下:

instance = null
460141958
460141958
true

从结果来看是同一个实例对象。

在这个实现中,我们通过将构造函数私有化,确保了外部无法通过new操作符来创建实例。而getInstance()方法则提供了一个全局访问点,通过懒加载的方式来创建实例,确保只有在需要使用时才会创建实例,从而节省资源。

这里需要注意的是,由于getInstance()方法是静态方法,所以需要将instance变量声明为静态变量

上述模式又叫懒汉式,是线程不安全的~

饿汉式(线程安全)

那么思考一下,上述不安全的问题主要存在于哪?

线程不安全问题主要是由于instance被多次实例化,那么采取直接实例化instance的方式就不会产生线程不安全问题。但是会浪费资源

// 饿汉式
private static Singleton01 instance = new Singleton01();


懒汉式(线程安全)

为了确保线程安全,那有什么办法让懒汉式线程安全呢?我们只需要对getInstance()方法进行同步加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了被多次实例化,因为加了,所以线程进入方法的时候就需要进行等待,性能上就会有有一点损耗

public static synchronized  Singleton01 getInstance() {
    if (instance == null) {
        System.out.println("instance = null");
        instance = new Singleton01();
    }
    return instance;
}

双重校验锁(线程安全)

instance 只需要被实例化一次之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 instance 没有被实例化时,才需要进行加锁。双重校验锁先判断 instance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。下面看下代码实现:

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

同时,我们也可以看到使用了volatile关键字,这个在之前的文章给大家详细讲过。这里简单给大家提一下,为什么用它~

Java中,由于JVM存在指令重排序线程可见性的问题,当一个线程在使用一个对象的时候,另外一个线程可能会看到一个不完整的对象状态,导致程序出现一些意想不到的错误。这个问题在多线程环境下非常常见。

为了解决这个问题,Java提供了一种关键字叫做volatile,它可以禁止JVM指令重排。它可以确保变量的可见性和有序性。在多线程环境下,当一个线程修改了volatile变量时,它会立即刷新到主存中,而其他线程在访问该变量时会强制从主存中重新读取最新的值,从而避免了读取到不完整的对象状态。

单例模式的实现中,由于instance变量在getInstance()方法中被多个线程共享,因此需要使用volatile关键字来确保变量的可见性和有序性,从而避免了多线程环境下的并发访问问题。

思考一下,这里为啥要使用两个if语句,明明在最外层已经判断了if (instance == null)而且里边已经加了了,在里边为什么还要if判断呢?

有时候,面试官会这么问?有的同学就答不上来了。大家不妨想象一下,当两个线程同时进入加锁的方法内,在没有判断的情况下instance对象还是会被实例化2次,因为代码块的语句是正常执行的,只是执行先后的问题~

静态内部类(线程安全)

Singleton03 类加载时,静态内部类 Singleton 没有被加载进内存。只有当调用 getInstance() 方法从而触发 Singleton.INSTANCESingleton 才会被加载,此时初始化 INSTANCE 实例。这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。

public class Singleton03 {
    private Singleton03() {
    }
    private static class Singleton {
        private static final Singleton03 INSTANCE = new Singleton03();
    }
    public static Singleton03 getInstance() {
        return Singleton.INSTANCE;
    }
}

枚举模式 (线程安全,最佳实践)

使用枚举实现单例模式是一种简洁而又安全的方式,这种方式可以避免多线程环境下的并发问题,同时也可以防止反射和反序列化攻击

在使用枚举实现单例模式时,只需要定义一个枚举类型,并在其中定义一个单例对象即可。由于枚举类型在Java中是天然的单例模式,因此这种方式可以保证在任何情况下都只创建一个实例对象

public enum Singleton04 {
    INSTANCE;
    private String message = "Hello World!";
    public void showMessage() {
        System.out.println(message);
    }
}

调用:

public class Application {
    public static void main(String[] args) {
        Singleton04.INSTANCE.showMessage();
    }
}

输出:

Hello World!

反射 & 反序列化攻击

反射攻击

有的小伙伴可能不知道,这里给大家扩展一下,下面通过一个简单的例子,看了之后就会明白了

反射攻击和反序列化攻击是两种常见的安全问题,它们都可以被用来攻击单例模式的实现。

反射攻击是指通过Java反射机制来获取类的私有构造方法,然后通过构造方法创建类的实例对象,从而破坏单例模式的实现。由于Java的反射机制可以访问私有的构造方法,因此攻击者可以通过这种方式来创建多个实例对象,从而破坏单例模式的唯一性。

public class Singleton05 {
    private static Singleton05 instance = new Singleton05();
    private Singleton05() {
        if (instance != null) {
            throw new IllegalStateException("Singleton already initialized");
        }
    }
    public static Singleton05 getInstance() {
        return instance;
    }
    public static void main(String[] args) throws Exception {
        Constructor<Singleton05> constructor = Singleton05.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton05 instance1 = constructor.newInstance();
        Singleton05 instance2 = Singleton05.getInstance();
        System.out.println(instance1 == instance2);
    }
}

运行一下:

//        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.java.design.single.Singleton05.main(Singleton05.java:26)
//        Caused by: java.lang.IllegalStateException: Singleton already initialized
//        at com.java.design.single.Singleton05.<init>(Singleton05.java:15)
//  ... 5 more

好家伙,直接干报错,原因也很简单,因为利用反射修改了构造方法的访问权限,然后进行了实例化,当再次运行进入if (instance != null) 就会抛出异常

序列化攻击

反序列化攻击是指攻击者通过序列化反序列化技术来破坏单例模式的实现。攻击者可以通过序列化反序列化来创建多个实例对象,从而破坏单例模式的唯一性。这种攻击方式常常被用于分布式系统中,攻击者可以在一个系统中序列化一个对象,然后在另一个系统中反序列化该对象,从而创建多个实例对象。

下面通过一个简单例子来看一下:

public class Singleton06 implements Serializable {
    private static final long serialVersionUID = 1L;
    private static Singleton06 instance = new Singleton06();
    private Singleton06() {
        if (instance != null) {
            throw new IllegalStateException("Singleton already initialized");
        }
    }
    public static Singleton06 getInstance() {
        return instance;
    }
    public static void main(String[] args) throws Exception {
        Singleton06 instance1 = Singleton06.getInstance();
        // 将实例对象序列化到文件中
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        out.writeObject(instance1);
        out.close();
        // 从文件中反序列化出实例对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
        Singleton06 instance2 = (Singleton06) in.readObject();
        in.close();
        System.out.println(instance1 == instance2); // false
    }
}

输出为 false从而达到了破坏,既然问题知道了,那怎么去防止攻击呢?其实很简单, 为了防止反序列化攻击,可以在单例类中添加一个readResolve()方法,用来替换从反序列化流中反序列化出的对象,确保只有单例对象的引用被返回

public class Singleton06 implements Serializable {
    private static final long serialVersionUID = 1L;
    private static Singleton06 instance = new Singleton06();
    private Singleton06() {
        if (instance != null) {
            throw new IllegalStateException("Singleton already initialized");
        }
    }
    public static Singleton06 getInstance() {
        return instance;
    }
    // 保护措施
    protected Object readResolve() {
        return instance;
    }
    public static void main(String[] args) throws Exception {
        Singleton06 instance1 = Singleton06.getInstance();
        // 将实例对象序列化到文件中
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        out.writeObject(instance1);
        out.close();
        // 从文件中反序
        // 从文件中反序列化出实例对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
        Singleton06 instance2 = (Singleton06) in.readObject();
        in.close();
        System.out.println(instance1 == instance2); // true
    }
}

看下输出: true,在这个示例代码中,我们添加了一个readResolve()方法,该方法返回单例对象的引用。当从反序列化流中反序列化出一个对象时,该方法会被自动调用,从而确保只有单例对象的引用被返回。

结束语

下节给大家讲工厂模式~

本着把自己知道的都告诉大家,如果本文对您有所帮助,点赞+关注鼓励一下呗~

相关文章

项目源码(源码已更新 欢迎star⭐️)

Kafka 专题学习

项目源码(源码已更新 欢迎star⭐️)

ElasticSearch 专题学习

项目源码(源码已更新 欢迎star⭐️)

往期并发编程内容推荐

推荐 SpringBoot & SpringCloud (源码已更新 欢迎star⭐️)

博客(阅读体验较佳)





相关文章
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
29 2
|
6天前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
19 4
|
15天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
23天前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
16 1
|
5天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
16 0
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和复用性的关键技术之一。本文将通过探讨单例模式,一种最常用的设计模式,来揭示其在PHP中的应用及优势。单例模式确保一个类仅有一个实例,并提供一个全局访问点。通过实际案例,我们将展示如何在PHP项目中有效实现单例模式,以及如何利用这一模式优化资源配置和管理。无论是PHP初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和技巧,进而提升自己的编程实践。
|
1月前
|
设计模式 安全 Java
C# 一分钟浅谈:设计模式之单例模式
【10月更文挑战第9天】单例模式是软件开发中最常用的设计模式之一,旨在确保一个类只有一个实例,并提供一个全局访问点。本文介绍了单例模式的基本概念、实现方式(包括饿汉式、懒汉式和使用 `Lazy&lt;T&gt;` 的方法)、常见问题(如多线程和序列化问题)及其解决方案,并通过代码示例详细说明了这些内容。希望本文能帮助你在实际开发中更好地应用单例模式,提高代码质量和可维护性。
31 1
|
1月前
|
设计模式 缓存 数据库连接
探索PHP中的设计模式:单例模式的实现与应用
在PHP开发中,设计模式是提高代码可复用性、可维护性和扩展性的重要工具。本文将深入探讨单例模式(Singleton Pattern)的基本概念、在PHP中的实现方式以及实际应用场景。单例模式确保一个类仅有一个实例,并提供全局访问点。通过具体代码示例和详细解释,我们将展示如何在PHP项目中有效利用单例模式来解决实际问题,提升开发效率和应用性能。
|
1月前
|
设计模式 存储 测试技术
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发领域,设计模式是解决常见问题的最佳实践。本文将深入探讨单例模式,一种确保类只有一个实例的设计模式,并提供实际应用示例。我们将从单例模式的基本概念讲起,通过实际案例展示如何在PHP中实现单例模式,以及它在不同场景下的应用和优势。最后,我们会探讨单例模式的优缺点,帮助开发者在实际项目中做出明智的选择。
|
1月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP开发领域,设计模式是解决常见问题的高效方案集合。它们不是具体的代码,而是一种编码和设计经验的总结。单例模式作为设计模式中的一种,确保了一个类仅有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的基本概念、实现方式及其在PHP中的应用。
单例模式在PHP中的应用广泛,尤其在处理数据库连接、日志记录等场景时,能显著提高资源利用率和执行效率。本文从单例模式的定义出发,详细解释了其在PHP中的不同实现方法,并探讨了使用单例模式的优势与注意事项。通过对示例代码的分析,读者将能够理解如何在PHP项目中有效应用单例模式。

热门文章

最新文章

  • 1
    C++一分钟之-设计模式:工厂模式与抽象工厂
    43
  • 2
    《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
    48
  • 3
    C++一分钟之-C++中的设计模式:单例模式
    58
  • 4
    《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
    38
  • 5
    《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
    63
  • 6
    Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
    58
  • 7
    Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
    42
  • 8
    Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
    50
  • 9
    Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
    110
  • 10
    Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
    78
  • 下一篇
    无影云桌面