自学内容网 自学内容网

设计模式学习之——单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这个模式的主要目的是控制对象的创建,确保在程序的整个生命周期中,某个类只有一个实例被创建和使用。

(单例模式应该也是我们最熟悉的设计模式之一了,多少次面试环节中必问的问题之一,懒汉式、饿汉式、双重检查锁、线程安全、应用场景,等等....)

工作原理

  1. 私有构造函数:通过将类的构造函数声明为私有,防止外部代码通过new关键字创建类的实例。

  2. 静态变量:在类内部定义一个静态变量来存储类的唯一实例。这个变量是私有的,以防止外部直接访问。

  3. 静态方法:提供一个公共的静态方法,用于返回类的唯一实例。如果实例尚未创建,则在该方法中创建实例;如果实例已经存在,则直接返回该实例。

适用场景

  • 当一个类只能有一个实例时,例如配置管理类、线程池等。
  • 当需要控制资源的使用,并且希望资源在全局范围内共享时。
  • 当需要实现全局唯一的服务或功能时,例如日志记录、数据库连接池等。

注意事项

  • 单例模式可能会引入全局状态,导致测试和维护变得更加困难。
  • 在多线程环境中,需要特别注意线程安全问题,确保实例的唯一性。
  • 在某些情况下,单例模式可能会导致内存泄漏,特别是当单例对象持有大量资源或引用其他生命周期较短的对象时。因此,在不再需要单例对象时,应该考虑如何正确地释放资源。

实现方式

单例模式的实现有多种方式,包括懒汉式、饿汉式、双重检查锁(Double-Checked Locking)等。以下是几种常见实现的简要说明:

  • 懒汉式(Lazy Initialization)
    • 在第一次调用getInstance()方法时创建实例。
    • 需要在方法中添加同步块以确保线程安全,但可能会影响性能。
public class Singleton {
    private static Singleton instance;

    // 私有构造函数
    private Singleton() {}

    // 公共静态方法,提供全局访问点
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

注意:上面的代码在每次调用getInstance()时都会进行同步,这可能会导致性能问题。可以使用双重检查锁来优化。

  • 饿汉式(Eager Initialization)
    • 在类加载时就创建实例。
    • 线程安全,因为JVM在加载类时会自动进行同步。
public class Singleton {
    // 类加载时就创建实例
    private static final Singleton instance = new Singleton();

    // 私有构造函数
    private Singleton() {}

    // 公共静态方法,提供全局访问点
    public static Singleton getInstance() {
        return instance;
    }
}
  • 双重检查锁(Double-Checked Locking)
    • 结合了懒汉式和饿汉式的优点,既延迟了实例化,又提高了性能。
    • 通过两次检查instance是否为null,并在第二次检查时添加同步块来确保线程安全。
public class Singleton {
    // 使用volatile关键字确保instance变量的可见性和有序性
    private static volatile Singleton instance;

    // 私有构造函数
    private Singleton() {}

    // 公共静态方法,提供全局访问点
    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  •  静态内部类(Bill Pugh Singleton Design)
    • 特点:利用类加载机制保证线程安全,同时实现延迟加载。
    • 实现:将单例实例放在静态内部类中,通过静态内部类的加载机制来确保实例的唯一性。
public class Singleton {
    // 私有构造函数
    private Singleton() {}

    // 静态内部类,负责创建单例实例
    private static class SingletonHelper {
        // 静态变量存储实例,类加载时初始化
        private static final Singleton INSTANCE = new Singleton();
    }

    // 公共静态方法,提供全局访问点
    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
  • 枚举(Enum Singleton)
    • 特点:使用枚举来实现单例模式是最简洁且线程安全的方式,由JVM提供保障,防止通过反射破坏单例。
    • 实现:定义一个包含单个实例的枚举。
public enum Singleton {
    INSTANCE;

    // 其他方法和属性可以定义在这里
    public void someMethod() {
        // 实现方法逻辑
    }
}

实现注意事项

  • 饿汉式适合在类加载时就创建实例的场景,但如果实例创建开销较大且不需要立即使用,则可能浪费资源。
  • 懒汉式 + 同步方法虽然简单,但每次调用getInstance()都会进行同步,性能较差。
  • 双重检查锁定通过减少同步块的执行次数来提高性能,但实现相对复杂。
  • 静态内部类结合了饿汉式和懒汉式的优点,既实现了延迟加载,又保证了线程安全。
  • 枚举是最推荐的方式,因为它不仅简洁且线程安全,还能防止通过反射和序列化攻击破坏单例。

枚举线程安全单例的疑问

枚举(Enum)实现的线程安全的单例模式在Java中是一种既简洁又高效的方式。

为什么枚举单例是线程安全的?
  1. JVM保障
    Java中的枚举类型是由JVM特别处理的。当枚举类被加载到JVM时,JVM会确保枚举实例的唯一性。这意味着枚举实例在创建时就被JVM锁定,任何尝试通过反射或其他手段来修改枚举实例的行为都会被JVM阻止。

  2. 创建时机
    枚举实例是在类加载阶段由JVM创建的。由于类加载是线程安全的(类加载器在加载类时会使用同步机制),因此枚举实例的创建也是线程安全的。

  3. 不可变性
    枚举类型默认是不可变的(immutable)。这意味着枚举实例一旦创建,其状态(字段值)就不能被改变。这种不可变性进一步增强了枚举单例的线程安全性。

为什么枚举能保证单例?
  1. 实例的唯一性
    在枚举中,每个枚举常量都是该枚举类型的一个实例。由于枚举常量是在类加载时由JVM创建的,并且JVM保证了枚举常量的唯一性,因此我们可以确信枚举类型中只有一个指定的常量实例。

  2. 防止反射攻击
    在Java中,通过反射机制可以绕过私有构造函数来创建类的实例。然而,对于枚举类型,JVM提供了额外的保护,以防止通过反射来创建新的枚举实例。如果尝试通过反射来修改枚举实例的字段或创建新的枚举实例,JVM会抛出异常。

  3. 防止序列化攻击
    如果一个类实现了Serializable接口,那么在反序列化时可能会创建新的实例。但是,对于枚举类型,JVM在反序列化时会确保返回的是枚举类型中已有的实例,而不是创建新的实例。这是因为枚举的序列化机制是由JVM特别处理的,它会使用枚举常量的名称来恢复枚举实例,而不是通过默认的序列化机制。

示例代码(实现方式中已有)

下面是一个使用枚举实现单例模式的示例代码:

public enum Singleton {
    INSTANCE;

    // 可以在这里添加枚举实例的方法
    public void doSomething() {
        // 实现方法逻辑
    }
}

// 使用枚举单例
public class Main {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething();
    }
}

在这个示例中,Singleton枚举只有一个实例INSTANCE。由于枚举的上述特性,我们可以确信INSTANCE是唯一的,并且它的创建和访问都是线程安全的。

综上所述,枚举实现的单例模式在Java中是一种非常强大且简洁的方式。它利用了JVM对枚举的特殊处理来确保实例的唯一性和线程安全性,同时防止了通过反射和序列化来破坏单例的攻击。

常见面试题

在面试中,关于单例模式的问题通常涵盖其定义、实现方式、线程安全性、应用场景以及可能的攻击方式和防御措施等方面。以下是一些常见的问题及其答案:

1、定义与特点

问题:什么是单例模式?它用于解决什么问题?

答案

  • 单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
  • 它用于解决在系统中需要控制某个类的实例数量,确保全局只有一个实例被创建和使用的问题。
2、实现方式

问题:单例模式常见写法有几种?请列举并解释。

答案

  • 懒汉式:在第一次调用getInstance()方法时创建实例,需要处理多线程安全问题。
  • 饿汉式:在类加载时就创建实例,简单但不提供延迟初始化。
  • 双重检查锁定(Double-Checked Locking):在懒汉式的基础上增加了锁,用于在多线程环境中保护单例的唯一性。
  • 静态内部类:利用类加载机制保证初始化实例时只有一个线程,同时实现延迟加载。
  • 枚举:使用枚举方式实现单例模式是最简洁的方法,由JVM提供保障,防止通过反射破坏单例。
3、线程安全性

问题:如何保证单例模式的线程安全?

答案

  • 对于懒汉式单例,可以通过在getInstance()方法上添加synchronized关键字来保证线程安全,但可能会影响性能。
  • 双重检查锁定可以减少加锁的次数,提高性能,同时保证线程安全。但需要注意使用volatile关键字来确保实例变量的可见性和有序性。
  • 饿汉式单例在类加载时就创建实例,因此是线程安全的。
4、应用场景

问题:单例模式适用于哪些场景?

答案

  • 单例模式适用于需要全局共享的资源或服务,例如配置管理类、线程池、日志记录器、数据库连接池等。
  • 它还适用于控制资源的使用,确保资源在全局范围内被唯一访问和管理的场景。
5、可能的攻击方式与防御措施

问题:如何通过反射或序列化攻击单例模式?如何防御?

答案

  • 反射攻击:通过Java反射机制可以绕过私有构造函数创建类的实例。为了防御这种攻击,可以在私有构造函数中抛出异常或进行其他安全检查。
  • 序列化攻击:如果单例类实现了Serializable接口,在反序列化时可能会创建新的实例。为了防御这种攻击,可以在readResolve()方法中返回单例的唯一实例。
6、其他问题

问题

  1. 单例模式的两次检查锁是什么?
  2. 你如何阻止使用clone()方法创建单例实例的另一个实例?
  3. Java中的单例模式什么时候是非单例?

答案

  1. 双重检查锁(Double-Checked Locking)是在懒汉式单例的基础上,通过两次检查实例是否为空来减少加锁的次数,提高性能。第一次检查在同步块外,第二次检查在同步块内。
  2. 可以通过在单例类中重写clone()方法并抛出异常来阻止使用clone()方法创建新的实例。
  3. 在Java中,如果单例类没有正确处理反射或序列化攻击,或者在多线程环境中没有正确实现同步机制,那么单例模式可能会失效,导致创建多个实例。

以上是一些关于单例模式在面试中常见的问题及其答案。在面试中,除了回答这些问题外,还可以根据面试官的要求进一步讨论单例模式的优缺点、与其他设计模式的比较以及在实际项目中的应用等话题。


原文地址:https://blog.csdn.net/github_38727595/article/details/144341012

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!