设计模式学习之——单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这个模式的主要目的是控制对象的创建,确保在程序的整个生命周期中,某个类只有一个实例被创建和使用。
(单例模式应该也是我们最熟悉的设计模式之一了,多少次面试环节中必问的问题之一,懒汉式、饿汉式、双重检查锁、线程安全、应用场景,等等....)
工作原理
-
私有构造函数:通过将类的构造函数声明为私有,防止外部代码通过
new
关键字创建类的实例。 -
静态变量:在类内部定义一个静态变量来存储类的唯一实例。这个变量是私有的,以防止外部直接访问。
-
静态方法:提供一个公共的静态方法,用于返回类的唯一实例。如果实例尚未创建,则在该方法中创建实例;如果实例已经存在,则直接返回该实例。
适用场景
- 当一个类只能有一个实例时,例如配置管理类、线程池等。
- 当需要控制资源的使用,并且希望资源在全局范围内共享时。
- 当需要实现全局唯一的服务或功能时,例如日志记录、数据库连接池等。
注意事项
- 单例模式可能会引入全局状态,导致测试和维护变得更加困难。
- 在多线程环境中,需要特别注意线程安全问题,确保实例的唯一性。
- 在某些情况下,单例模式可能会导致内存泄漏,特别是当单例对象持有大量资源或引用其他生命周期较短的对象时。因此,在不再需要单例对象时,应该考虑如何正确地释放资源。
实现方式
单例模式的实现有多种方式,包括懒汉式、饿汉式、双重检查锁(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中是一种既简洁又高效的方式。
为什么枚举单例是线程安全的?
-
JVM保障:
Java中的枚举类型是由JVM特别处理的。当枚举类被加载到JVM时,JVM会确保枚举实例的唯一性。这意味着枚举实例在创建时就被JVM锁定,任何尝试通过反射或其他手段来修改枚举实例的行为都会被JVM阻止。 -
创建时机:
枚举实例是在类加载阶段由JVM创建的。由于类加载是线程安全的(类加载器在加载类时会使用同步机制),因此枚举实例的创建也是线程安全的。 -
不可变性:
枚举类型默认是不可变的(immutable)。这意味着枚举实例一旦创建,其状态(字段值)就不能被改变。这种不可变性进一步增强了枚举单例的线程安全性。
为什么枚举能保证单例?
-
实例的唯一性:
在枚举中,每个枚举常量都是该枚举类型的一个实例。由于枚举常量是在类加载时由JVM创建的,并且JVM保证了枚举常量的唯一性,因此我们可以确信枚举类型中只有一个指定的常量实例。 -
防止反射攻击:
在Java中,通过反射机制可以绕过私有构造函数来创建类的实例。然而,对于枚举类型,JVM提供了额外的保护,以防止通过反射来创建新的枚举实例。如果尝试通过反射来修改枚举实例的字段或创建新的枚举实例,JVM会抛出异常。 -
防止序列化攻击:
如果一个类实现了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、其他问题
问题:
- 单例模式的两次检查锁是什么?
- 你如何阻止使用
clone()
方法创建单例实例的另一个实例? - Java中的单例模式什么时候是非单例?
答案:
- 双重检查锁(Double-Checked Locking)是在懒汉式单例的基础上,通过两次检查实例是否为空来减少加锁的次数,提高性能。第一次检查在同步块外,第二次检查在同步块内。
- 可以通过在单例类中重写
clone()
方法并抛出异常来阻止使用clone()
方法创建新的实例。 - 在Java中,如果单例类没有正确处理反射或序列化攻击,或者在多线程环境中没有正确实现同步机制,那么单例模式可能会失效,导致创建多个实例。
以上是一些关于单例模式在面试中常见的问题及其答案。在面试中,除了回答这些问题外,还可以根据面试官的要求进一步讨论单例模式的优缺点、与其他设计模式的比较以及在实际项目中的应用等话题。
原文地址:https://blog.csdn.net/github_38727595/article/details/144341012
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!