Java单例模式:饿汉、懒汉、DCL三种实现及最佳实践

15 阅读6分钟

单例模式是Java设计模式中最基础也最常用的创建型模式,核心目标是保证一个类在程序运行期间只有一个实例,并提供全局访问点。本文将详细拆解三种经典单例实现方式(饿汉式、懒汉式、双重校验锁),结合代码示例分析各自的优缺点与适用场景。

一、单例模式的核心设计原则

在实现单例前,需先明确三个核心约束:

  1. 私有化构造器:禁止外部通过 new 关键字创建实例;
  2. 私有静态成员变量:存储类的唯一实例;
  3. 公共静态方法:提供全局访问入口,返回唯一实例。

所有单例实现都围绕这三个原则展开,区别仅在于实例创建时机线程安全处理方式

二、饿汉式单例(饿汉模式)

1. 核心特征

  • 创建时机:类加载时(ClassLoader加载类阶段)就完成实例化,属于“预加载”;
  • 线程安全:天然线程安全(类加载由JVM保证线程安全,只会执行一次);
  • 优点:实现简单、无并发问题;
  • 缺点:可能造成资源浪费(实例创建后未被使用,也会占用内存)。

2. 经典代码实现

饿汉式有两种常见写法:静态变量版静态代码块版,本质逻辑一致。

写法1:静态变量直接初始化

/**
 * 饿汉式单例 - 静态变量版
 * 类加载时直接创建实例,线程安全但可能浪费资源
 */
public class HungrySingleton {
    // 1. 私有静态成员变量:类加载时初始化,全局唯一
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    // 2. 私有化构造器:禁止外部new
    private HungrySingleton() {
        // 防止反射破坏单例(可选增强)
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }
    }

    // 3. 公共静态方法:提供全局访问入口
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }

    // 测试方法
    public void sayHello() {
        System.out.println("饿汉式单例:Hello Singleton!");
    }
}

写法2:静态代码块初始化(适合复杂初始化逻辑)

/**
 * 饿汉式单例 - 静态代码块版
 * 适合需要加载配置文件、初始化资源等复杂场景
 */
public class HungrySingletonWithStaticBlock {
    // 1. 私有静态成员变量(先声明,后初始化)
    private static final HungrySingletonWithStaticBlock INSTANCE;

    // 2. 静态代码块:类加载时执行,完成实例初始化
    static {
        try {
            // 模拟复杂初始化逻辑(如读取配置文件)
            INSTANCE = new HungrySingletonWithStaticBlock();
        } catch (Exception e) {
            throw new RuntimeException("单例初始化失败", e);
        }
    }

    // 3. 私有化构造器
    private HungrySingletonWithStaticBlock() {
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }
    }

    // 4. 公共静态方法
    public static HungrySingletonWithStaticBlock getInstance() {
        return INSTANCE;
    }

    public void sayHello() {
        System.out.println("饿汉式单例(静态代码块):Hello Singleton!");
    }
}

3. 测试代码

public class SingletonTest {
    public static void main(String[] args) {
        // 饿汉式测试
        HungrySingleton instance1 = HungrySingleton.getInstance();
        HungrySingleton instance2 = HungrySingleton.getInstance();
        System.out.println(instance1 == instance2); // true(同一实例)
        instance1.sayHello();
    }
}

三、懒汉式单例(懒汉模式)

1. 核心特征

  • 创建时机:第一次调用 getInstance() 方法时才实例化,属于“懒加载”;
  • 线程安全:基础版非线程安全,需加锁保证安全;
  • 优点:延迟加载,避免资源浪费;
  • 缺点:加锁后会降低并发性能。

2. 经典代码实现

写法1:基础版(非线程安全,禁止生产使用)

/**
 * 懒汉式单例 - 基础版(非线程安全)
 * 仅适合单线程环境,多线程下会创建多个实例
 */
public class LazySingletonUnsafe {
    // 1. 私有静态成员变量(不初始化)
    private static LazySingletonUnsafe INSTANCE;

    // 2. 私有化构造器
    private LazySingletonUnsafe() {}

    // 3. 公共静态方法:第一次调用时创建实例
    public static LazySingletonUnsafe getInstance() {
        if (INSTANCE == null) { // 判空:未创建则初始化
            INSTANCE = new LazySingletonUnsafe();
        }
        return INSTANCE;
    }
}

写法2:加锁版(线程安全,性能较低)

/**
 * 懒汉式单例 - 加锁版(线程安全)
 * 用synchronized保证线程安全,但每次调用都加锁,性能差
 */
public class LazySingletonSafe {
    private static LazySingletonSafe INSTANCE;

    private LazySingletonSafe() {}

    // 方法上加synchronized,保证同一时间只有一个线程执行
    public static synchronized LazySingletonSafe getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingletonSafe();
        }
        return INSTANCE;
    }

    public void sayHello() {
        System.out.println("懒汉式单例(加锁版):Hello Singleton!");
    }
}

3. 问题分析

  • 基础版在多线程下,多个线程同时进入 if (INSTANCE == null) 会创建多个实例,破坏单例;
  • 加锁版虽然线程安全,但 synchronized 加在方法上,每次调用 getInstance() 都会加锁,即使实例已创建,仍会有锁竞争,性能损耗大。

四、双重校验锁(DCL)单例

1. 核心特征

  • 创建时机:懒加载(第一次调用时创建);
  • 线程安全:双重判空 + 局部锁,兼顾线程安全与性能;
  • 优点:懒加载 + 线程安全 + 高性能(仅实例创建时加锁);
  • 注意点:需给实例变量加 volatile 关键字,防止指令重排。

2. 经典代码实现

/**
 * 双重校验锁(DCL)单例 - 懒加载 + 线程安全 + 高性能
 * 生产环境最常用的单例实现方式
 */
public class DclSingleton {
    // 1. 私有静态成员变量:加volatile防止指令重排
    private static volatile DclSingleton INSTANCE;

    // 2. 私有化构造器
    private DclSingleton() {
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }
    }

    // 3. 公共静态方法:双重判空 + 局部锁
    public static DclSingleton getInstance() {
        // 第一次判空:实例已创建时,直接返回,无需加锁(提升性能)
        if (INSTANCE == null) {
            // 局部锁:仅实例未创建时加锁,减少锁竞争
            synchronized (DclSingleton.class) {
                // 第二次判空:防止多个线程等待锁后重复创建实例
                if (INSTANCE == null) {
                    INSTANCE = new DclSingleton();
                }
            }
        }
        return INSTANCE;
    }

    public void sayHello() {
        System.out.println("DCL单例:Hello Singleton!");
    }
}

3. 关键知识点:volatile的作用

INSTANCE = new DclSingleton() 并非原子操作,JVM会拆分为3步:

  1. 分配内存空间;
  2. 初始化实例对象;
  3. 将INSTANCE指向分配的内存地址。

若无 volatile,JVM可能发生指令重排(步骤2和3交换),导致线程A创建实例时,线程B看到 INSTANCE != null,但实例未完成初始化,进而获取到“半初始化”的实例,引发空指针异常。

volatile 关键字会禁止指令重排,保证实例创建的完整性。

4. 测试代码

public class SingletonTest {
    public static void main(String[] args) {
        // DCL单例测试(多线程环境)
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                DclSingleton instance = DclSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + ":" + instance);
            }).start();
        }
        // 输出结果:所有线程获取的是同一个实例
    }
}

五、三种单例模式对比与选型建议

实现方式线程安全懒加载性能适用场景
饿汉式实例占用资源少、启动即使用
懒汉式(加锁)单线程环境、对性能要求低
双重校验锁(DCL)多线程环境、高性能要求(推荐)

选型核心原则:

  1. 简单场景:优先饿汉式(实现简单,无并发问题);
  2. 资源敏感场景:优先DCL(懒加载+高性能);
  3. 禁止使用:懒汉式基础版(非线程安全);
  4. 特殊场景:若需防止反射/序列化破坏单例,可使用枚举单例(《Effective Java》推荐,本文暂不展开)。

六、总结

单例模式的核心是“唯一实例 + 全局访问”,三种经典实现各有侧重:

  • 饿汉式胜在简单、线程安全,但牺牲了懒加载;
  • 懒汉式实现了懒加载,但基础版不安全,加锁版性能差;
  • 双重校验锁(DCL)是平衡“懒加载、线程安全、高性能”的最优解,也是生产环境中最常用的单例写法。