单例模式实现全解
实现1:静态方法
public final class Singleton implements Serializable {
private Singleton() {}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
问题1:为什么加 final
-
final
关键字用于声明Singleton
类不能被继承。这样可以防止其他类继承Singleton
类并可能破坏单例模式。
问题2:如果实现了序列化接口,还要做什么来防止反序列化破坏单例
-
即使
Singleton
类实现了Serializable
接口,仅仅这样还不足以防止反序列化破坏单例。在反序列化过程中,如果没有额外的处理,可能会创建一个新的Singleton
实例。 -
为了解决这个问题,
Singleton
类提供了readResolve()
方法。当反序列化Singleton
对象时,readResolve()
方法会被调用,它返回单例实例的引用,确保反序列化不会创建新的实例。
问题3:为什么设置为私有?是否能防止反射创建新的实例?
-
构造函数被设置为私有是为了防止外部通过
new
关键字直接创建Singleton
类的新实例。 -
然而,私有构造函数并不能完全防止反射攻击。通过反射,可以访问私有构造函数并使用它来创建新的实例。为了防止这种情况,通常需要在构造函数中添加安全检查,或者使用其他机制(如枚举)来实现单例。
问题4:这样初始化是否能保证单例对象创建时的线程安全?
-
INSTANCE
变量被声明为private static final
,并且是通过直接赋值的方式初始化的。在Java中,对于final
字段,如果它是一个不可变对象(如String
、基本数据类型包装类或枚举),那么它会被初始化为一个常量表达式,并且在类加载时就已经完成初始化。 -
因此,
INSTANCE
的初始化是线程安全的,它在类加载时就已经被创建,且不可变。这确保了单例对象的创建是线程安全的。
问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public
-
将
INSTANCE
设置为public
会允许外部直接访问单例实例,这可能会导致单例模式被破坏,例如外部可以修改单例实例的状态。 -
通过提供一个公共的静态方法
getInstance()
来访问单例实例,可以在访问时添加额外的逻辑,例如安全检查或延迟初始化。 -
使用静态方法还可以在未来更改单例的实现,而不影响使用单例的客户端代码,因为客户端代码依赖的是方法签名,而不是具体的字段。
实现2:枚举
enum Singleton {
INSTANCE;
}
使用枚举实现单例是一种简洁且线程安全的方式。以下是对您提出问题的回答:
问题1:枚举单例是如何限制实例个数的
-
在Java中,枚举类型本身是单例的。每个枚举常量在Java虚拟机(JVM)中只会有一个实例。
问题2:枚举单例在创建时是否有并发问题
-
枚举实例在JVM加载枚举类时创建,因此它们是线程安全的。即使多个线程尝试同时访问枚举实例,JVM也会保证只有一个实例被创建。
问题3:枚举单例能否被反射破坏单例
-
由于枚举常量是静态且不可变的,通过反射创建新实例是不可能的。Java语言规范确保了枚举常量的唯一性。
问题4:枚举单例能否被反序列化破坏单例
-
枚举类型的反序列化过程由JVM控制,它保证了反序列化后的枚举实例与枚举类中定义的实例相同,不会破坏单例模式。
问题5:枚举单例属于懒汉式还是饿汉式
-
枚举单例实际上不属于懒汉式或饿汉式。它们在类加载时就实例化,类似于饿汉式,但由于枚举的特殊性,它们在任何情况下都保证只有一个实例。
问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
-
可以在枚举常量中定义方法来执行初始化逻辑。由于枚举常量在JVM加载枚举类时就创建,你可以在枚举常量中放置初始化代码或调用一个初始化方法。
public enum Singleton {
INSTANCE;
// 初始化逻辑可以在这里定义
private Resource resource;
// 枚举实例的构造函数
Singleton() {
resource = new Resource();
// 其他初始化逻辑
}
public Resource getResource() {
return resource;
}
// 其他方法
}
在这个例子中,Singleton
枚举类型有一个名为 INSTANCE
的实例,它在类加载时创建。构造函数 Singleton()
中包含了初始化逻辑。通过 getResource()
方法可以访问初始化后的资源。
枚举单例是一种非常强大和推荐的方式来实现单例模式,因为它提供了语言层面的保证,确保了单例的唯一性和线程安全性。
实现3:单重检查锁
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
实现4:双重检查锁
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
问题1:为什么要加 volatile
?
在Java中,volatile
关键字用于确保变量的读写操作对所有线程都是可见的,并且每次读取都能获取到最新的值。在多线程环境中,如果不使用 volatile
,可能会出现指令重排的问题,导致其他线程看不到单例实例已经被初始化。
具体来说,在没有 volatile
的情况下,创建单例对象 Singleton
的过程涉及几个步骤:
-
分配内存空间。
-
初始化对象。
-
设置
INSTANCE
引用指向分配的内存地址。
由于Java内存模型允许指令重排,上述步骤可能会被重排到第三步之后执行,这意味着在 INSTANCE
引用被设置之前,其他线程可能会看到部分初始化的单例实例,这将导致错误的状态被其他线程观察到。
使用 volatile
关键字可以防止这种指令重排,确保在 INSTANCE
引用被设置之前,对象的初始化工作已经完成。
问题2:为什么要双重检查锁?
双重检查锁定是一种减少获取锁开销的技术。在第一次检查 INSTANCE != null
时,如果没有单例实例,线程将获取锁。在获取锁之后,进行第二次检查 INSTANCE != null
来确保在此期间没有其他线程已经创建了实例。这样可以减少在单例实例已经创建后,其他线程获取锁的频率,从而提高性能。
问题3:为什么还要在这里加为空判断,之前不是判断过了吗?
双重检查锁定中的第二次检查是必要的,因为即使第一次检查确认 INSTANCE
是 null
,也可能有其他线程在当前线程进入同步块之前已经创建了单例实例。
考虑以下场景:
-
线程 T1 发现
INSTANCE
是null
。 -
线程 T1 准备获取锁,但此时线程调度器决定暂时挂起 T1。
-
线程 T2 恢复并发现
INSTANCE
是null
,然后获取锁并创建了单例实例。 -
线程 T2 释放锁。
-
线程 T1 恢复,进入同步块,并再次检查
INSTANCE
,此时它仍然是null
,因此 T1 会创建一个新的实例,这是错误的。
通过在同步块内进行第二次检查,可以确保即使在上述情况下,也不会创建多个实例。
(推荐的)实现5:内部类->类加载时初始化
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式 懒汉式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题 JVM保证其安全性
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
问题1:属于懒汉式还是饿汉式?
这个实现属于懒汉式。在懒汉式单例模式中,实例是在第一次调用 getInstance()
方法时创建的,而不是在类加载时就创建。这种方式的优点是可以推迟对象的创建直到真正需要它的时候,从而减少内存占用。缺点是必须适当地同步 getInstance()
方法以确保线程安全,或者使用其他机制来保证只创建一个实例。
问题2:在创建时是否有并发问题?
在这个特定的实现中,没有并发问题。原因是 Java 保证了一个类的加载过程是线程安全的。当一个类第一次被主动使用时,JVM 会同步地加载这个类。由于 LazyHolder
是 Singleton
类的内部静态类,它只有在第一次调用 getInstance()
方法时才会被加载。此时,JVM 会保证只有一个线程可以创建 LazyHolder
类的实例。
此外,由于 INSTANCE
是 LazyHolder
类的静态成员,并且是在静态初始化块中初始化的,Java 也保证了静态初始化块的执行是线程安全的。这意味着 LazyHolder.INSTANCE
的创建对所有线程来说是原子的,并且只执行一次。
这种实现方式通常被称为“比尔·波恩(Bill Pugh)的单例模式”,它结合了懒汉式和饿汉式单例模式的优点:它既实现了延迟加载,又避免了同步带来的性能开销,并且是线程安全的。
总结来说,这个 Singleton
类实现了一个线程安全的懒汉式单例模式,且由于 JVM 对类加载过程的内部细节保证,创建实例时不会有并发问题。
为什么更推荐实现5:LazyHolder
方式
推荐使用 LazyHolder
方式(也称为比尔·波格单例模式)而不是双重检查锁定(DCL)的主要原因在于线程安全性和简洁性。
LazyHolder
方式相对于双重检查锁定的一些优势:
-
线程安全性:
-
比尔·波格单例模式:利用了 Java 的类加载机制来确保线程安全。静态内部类
LazyHolder
只有在第一次使用时才会加载,Java 保证了一个类的加载过程是线程安全的,因此不需要额外的同步措施。 -
双重检查锁定:在多线程环境中,双重检查锁定可能会遇到可见性和指令重排的问题,导致不正确的行为。虽然在 Java 5 及以后的版本中,
volatile
关键字的使用可以保证可见性,但双重检查锁定的正确实现仍然相对复杂。
-
-
简洁性:
-
比尔·波格单例模式:提供了一种更简洁的方式来实现单例,代码更易于理解和维护。
-
双重检查锁定:需要使用
volatile
关键字,并且要正确管理同步块,这增加了代码的复杂性。
-
-
性能:
-
比尔·波格单例模式:不需要在每次获取实例时进行同步,减少了性能开销。
-
双重检查锁定:虽然减少了同步的使用,但仍然需要在
getInstance()
方法中进行两次检查,可能会有轻微的性能影响。
-
-
避免复杂的锁定机制:
-
比尔·波格单例模式:避免了复杂的锁定机制,只依赖于 Java 的类加载机制。
-
双重检查锁定:需要手动管理锁,增加了出错的可能性。
-
-
避免反射和序列化问题:
-
比尔·波格单例模式:更难通过反射攻击破坏单例模式,因为单例实例是通过私有的静态内部类创建的。
-
双重检查锁定:可能更容易受到反射攻击,因为单例实例的创建依赖于公共的构造函数。
-
-
易于扩展:
-
比尔·波格单例模式:如果需要修改单例的创建逻辑,只需修改
LazyHolder
类即可。 -
双重检查锁定:任何修改都可能需要重新审视同步机制,以确保线程安全。
-
netty中单例模式
Netty 中使用饿汉式单例模式的实践很常见,因为这种模式简单且线程安全,适合用于创建全局的、只被实例化一次的对象。以下是一些 Netty 中使用饿汉式单例模式的源码示例:
-
选择策略对象DefaultSelectStrategy:
final class DefaultSelectStrategy implements SelectStrategy { static final SelectStrategy INSTANCE = new DefaultSelectStrategy(); private DefaultSelectStrategy() { } @Override public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception { return hasTasks ? selectSupplier.getAsInt() : SelectStrategy.SELECT; } }
-
MQTT消息编码器MqttEncoder:
public class MqttEncoder { public static final MqttEncoder INSTANCE = new MqttEncoder(); private MqttEncoder() { } // Encoder methods... }
-
读超时异常类ReadTimeoutException:
public final class ReadTimeoutException extends IOException { public static final ReadTimeoutException INSTANCE = new ReadTimeoutException(); private ReadTimeoutException() { super("read timed out"); } }
此外还有WriteTimeoutException、ClosedChannelException等