啥是设计模式?
设计模式好比象棋中的 “棋谱”。红方当头炮,黑方马来跳。针对红方的一些走法,黑方应招的时候有一些固定的套路。按照套路来走局势就不会吃亏。
软件开发中也有很多常见的 “问题场景”。针对这些问题场景,大佬们总结出了一些固定的套路。按照这个套路来实现代码,也不会吃亏。
一、饿汉式:简单粗暴的线程安全
饿汉式单例模式的核心思路是在类加载时就完成实例的创建,将实例化过程提前,从而避免多线程环境下可能出现的并发问题。Java 代码如下:
public class Singleton {
// 类加载时立即初始化实例
private static final Singleton instance = new Singleton();
// 私有化构造函数,防止外部实例化
private Singleton() {}
// 提供全局访问点
public static Singleton getInstance() {
return instance;
}
}
在上述代码中,instance 在类加载阶段就被赋值为 Singleton 类的实例。由于 Java 中类加载机制是线程安全的,当多个线程同时访问 getInstance 方法时,获取到的始终是同一个实例,不存在多个线程同时创建实例的情况。
然而,这种方式的弊端也很明显。即使程序暂时不需要使用单例实例,实例也会在类加载时被创建,这可能造成内存资源的浪费。因此,饿汉式单例模式更适用于实例创建开销较小,且程序启动时就需要使用该实例的场景。
二、懒汉式:双重检查锁的精妙平衡
类加载的时候不创建实例。第一次使用的时候才创建实例。
Java单线程版代码如下:
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面的懒汉模式的实现是线程不安全的。
线程安全问题发生在首次创建实例时,如果在多个线程中同时调用getInstance方法,就可能导致创建出多个实例。
一旦实例已经创建好了,后面再多线程环境调用getInstance就不再有线程安全问题了(不在修改instance了)
加上synchronized可以改善这里的线程安全问题。
Java多线程版代码如下:
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面的多线程代码仍有改进的空间,一下代码在枷锁的基础上,做出了进一步改动:
使用双重if判定,降低锁竞争的频率/
给instance加上了volatile。
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
理解双重if判定/volatile:
加锁/解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候,因此后续使用的时候,不必在进行加锁了。
外层的 if 就是判定下当前是否已经把instance实例创建出来了。
同时为了避免"内存可见性"导致读取的instance出现偏差,于是补充说voiatile。
当多线程首次调用getinstance,大家可能都发现instance为null,于是又继续往下执行来竞争锁,其中竞争成功的线程,在完成创建实例的操作。
当这个实例创建完成了之后,其他竞争到锁的线程就被里层 if 挡住了,也就不会继续创建其他实例。
1.有三个线程,开始执行getInstance,通过外层的if(instance == null)知道了实例还没有创建的消息,于是开始竞争同一把锁。
2.其中线程1率先获取到锁,此时线程1通过里层的 if (instance ==null) 进一步确认实例是否已经创建,如果没创建,就把这个实例创建出来。
3.当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的 if (instance == null)来确认实例是否已经创建,发现实例已经创建出来了,就不再创建了。
4.后续的线程,不必加锁,直接就通过外层if (instance == null)就知道实例已经创建了,从而不再尝试获取锁了,降低了开销。
三、静态内部类:JVM 保驾护航的优雅方案
静态内部类方式在 Java 中应用广泛,它巧妙地利用了 JVM 的类加载机制来实现线程安全与延迟初始化的完美结合。Java 代码示例如下:
public class Singleton {
// 私有化构造函数
private Singleton() {}
// 静态内部类,在外部类被加载时不会被加载
private static class SingletonHolder {
// 静态常量,在内部类被加载时创建实例
private static final Singleton INSTANCE = new Singleton();
}
// 提供全局访问点
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
在上述代码中,Singleton 类的构造函数被私有化,防止外部直接实例化。SingletonHolder 是 Singleton 的静态内部类,其中定义了静态常量 INSTANCE 。当 Singleton 类被加载时,SingletonHolder 类并不会被立即加载,只有当调用 getInstance 方法时,SingletonHolder 类才会被加载,此时 INSTANCE 会被创建。由于 JVM 保证类加载过程的线程安全,所以静态内部类方式既保证了线程安全,又实现了延迟初始化,是一种非常优雅的实现方式 。
四、枚举:简洁高效的终极方案
在 Java 中,使用枚举来实现单例模式是一种非常简洁且安全的方式,代码如下:
public enum Singleton {
INSTANCE;
// 可以在枚举中定义其他方法和属性
public void doSomething() {
System.out.println("Singleton instance is doing something.");
}
}
在上述代码中,Singleton 枚举类型仅有一个成员 INSTANCE ,它就是单例实例。枚举类型天生支持线程安全,并且自动处理了序列化和反序列化问题,防止通过反序列化创建多个实例。在使用时,直接通过 Singleton.INSTANCE 访问实例,非常方便。此外,还可以在枚举中定义其他方法和属性,进一步扩展单例的功能。
五、总结与选择建议
以上介绍了多种 Java 线程安全的单例模式实现方式,每种方式都有其独特的优势和适用场景。饿汉式简单直接,但可能造成资源浪费;懒汉式双重检查锁实现了性能与安全的平衡,但需要正确使用 volatile 关键字;静态内部类借助 JVM 实现优雅的线程安全与延迟初始化;枚举方式简洁高效,尤其适合处理序列化场景。
在实际项目中,开发者应根据具体需求选择合适的单例模式实现方式。如果对资源消耗不敏感,追求简单性,饿汉式是不错的选择;若需要延迟初始化且注重性能,懒汉式双重检查锁或静态内部类更为合适;而对于涉及序列化和反序列化的场景,枚举方式则是最佳实践。掌握这些单例模式的实现技巧,将有助于我们编写出更加健壮、高效的 Java 代码。