目录
一、什么是单例模式:
一个类,在语法角度来说,是可以无限创建实例的。
但是,在一些实际场景中,我们有时候希望这个类只有唯一的实例,这就是单例模式。
那么,有哪些单例模式呢?
这里着重讲解两种单例模式;饿汉模式 和 懒汉模式。
二、饿汉模式:
饿汉模式采取 " 急切 " 创建实例的策略,也就是在类加载的时候(可以近似理解为程序启动的时候)就自动创建该类的唯一实例,无论这个实例在后续程序运行中是否会被使用,它都会在类加载阶段被创建出来。
代码示例:
java
class EagerSingleton {
// 类加载时直接初始化实例
private static final EagerSingleton instance = new EagerSingleton();
// 私有构造函数
private EagerSingleton() {
}
// 提供全局访问点
public static EagerSingleton getInstance() {
return instance;
}
}
静态常量 instance : private static final EagerSingleton instance = new EagerSingleton();
这行代码在类加载的时候就会执行,从而创建 EagerSingleton 这个类的唯一实例。static 关键字保证了该实例是类级别的,被所有对象共享; final 关键字保证了该实例一旦被创建就不可再改变。
私有构造函数 : private EagerSingleton() { }
将构造函数设为私有,这样外部代码就无法通过 new 关键字来创建 EagerSingleton 这个类的新实例,从而保证了单例的唯一性。(因为被 private 关键字修饰的成员只能在其所在的类内部被访问,其他类无法直接访问)
静态方法:public static EagerSingleton getInstance() { }
是一个公共的静态方法,用于提供全局访问点,外部代码可以通过调用这个方法来获取EagerSingleton 这个类的唯一实例。
饿汉模式的特点:
线程安全:
在饿汉模式里,单例实例在类加载时就被创建好了。后续多个线程调用 EagerSingleton.getInstance() 方法时,只是简单地返回已经存在的 instance
实例引用,不涉及任何可能导致线程安全问题的操作(如对共享资源的读写操作)。所以,饿汉模式天然具备线程安全性,不需要像一些其他单例实现方式那样使用额外的同步机制(如 synchronized
关键字)来保证线程安全。
得到的引用相同:
java
class EagerSingleton {
// 类加载时直接初始化实例
private static final EagerSingleton instance = new EagerSingleton();
// 私有构造函数
private EagerSingleton() {
}
// 提供全局访问点
public static EagerSingleton getInstance() {
return instance;
}
}
public class Demo12 {
public static void main(String[] args) {
EagerSingleton e1 = EagerSingleton.getInstance();
EagerSingleton e2 = EagerSingleton.getInstance();
System.out.println(e1 == e2);
}
}

但是如果有人尝试通过new关键字获取实例就会编译出错:

三、懒汉模式:
在JAVA中,懒汉模式的核心思想是延迟实例的创建 ,直到第一次真正需要使用时才初始化对象 。这种设计可以避免资源浪费,但需特别注意线程安全性问题。
错误代码:
java
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
上述代码中, instance 初始为 null ,在 getInstannce 方法里,先检查 instance 是否为 null ,若为 null 则创建实例,否则直接返回已有实例。
上述基本实现的懒汉模式在单线程环境下能正常工作,但在多线程环境中存在线程安全问题。假设有两个线程 A 和 B 同时调用 getInstannce 方法,当 A 执行到 if (instance == null) 时,
判断结果为 true ,在 A 还未创建实例前, B 也执行到了该判断语句,此时 instance 仍为 null ,B 的判断结果也为 true ,这样A 和 B 都会创建新的实例,从而破坏了单例模式的唯一性。
代码改进:(下述代码还是不够好,会有多余的性能浪费,对上述的代码进行了些许改进)
java
class LazySingletonSyncMethod {
private static LazySingletonSyncMethod instance = null;
private LazySingletonSyncMethod() {
}
//此处改进了代码
public static synchronized LazySingletonSyncMethod getInstance() {
if (instance == null) {
instance = new LazySingletonSyncMethod();
}
return instance;
}
}
通过给 getInstance 方法添加 synchronized 关键字,保证了同一时刻只有一个线程能进入该方法,解决了线程安全问题。但这种方式会导致每次调用 getInstance 方法都进行同步,会带来不必要的性能开销,因为只有在创建实例时才需要同步。
正确代码示例(双重检查锁):
java
class LazySingletonDCL {
//加上 volatile 关键字修饰
private static volatile LazySingletonDCL instance = null;
private LazySingletonDCL() {
}
//双重检查
public static LazySingletonDCL getInstance() {
if (instance == null) {
synchronized (LazySingletonDCL.class) {
if (instance == null) {
instance = new LazySingletonDCL();
}
}
}
return instance;
}
}
双重检查锁定先检查 instance 是否为 null ,若为 null 则进入 锁块 (synchronized (){ }),进入锁块后再次检查 instance 是否为 null ,若还是 null 才创建实例。同时,使用 volatiile 关键字保证 instance 变量的可见性,避免在多线程环境下出现问题。
volatile 关键字也就是防止第一个线程进入创建完线程后,防止第二个线程因为内存可见性,没发现实例已经被创建了而再次创建了第二个实例,从而破坏了单例模式的唯一性。
volatile 关键字还可以禁止指令重排序, 因为 instance = new LazySingletonDCL()这行代码在 JVM 中并不是一个原子操作,它大致会分为以下三个步骤:
|-------------------------------------------------|
| **1、分配内存空间:**为 LazySingletonDCL 对象分配内存。 |
| **2、初始化对象:**调用 LazySingletonDCL 的构造函数,对对象进行初始化。 |
| 3、将引用指向对象:将 instance 引用指向刚分配的内存地址。 |
在没有 volatile 修饰的情况下,JVM 为了优化性能,可能会对上述步骤进行指令重排序,在还没有 LazySingletonDCL类 实例的时候 , 比如先执行步骤 1 和 3,再执行步骤 2 。这样在第一个线程执行完步骤 3 但 还未执行步骤 2 时, instance 引用已经指向了一个非 null 的内存地址,但对象还未完成初始化。此时如果第二个线程进入 getInstance 方法,进行第一次 if (instance == null) 检查(也就是最外层的 if 判断条件)时,会发现 instance 不为 null ,就会直接返回 instance ,但实际上这个对象还未完成初始化,第二个线程后续使用该对象时可能会因为访问到的成员变量可能还未被正确赋值,从而可能出现空指针异常等问题。
当使用 volatile修饰 instance 变量后,JVM 会禁止对 instance 相关的指令进行重排序,保证步骤 1、2、3 按顺序执行,从而避免上述问题。