Java单例模式:预加载与延迟初始化的实现

40 阅读6分钟

Java单例模式:预加载与延迟初始化的实现

大家好!今天就结合自己的学习经历,来和大家分享一下Java中单例模式的两种经典实现方式。文章里会包含我踩过的坑、学习时的困惑,以及如何正确使用它们。

一、先搞懂:单例模式的核心设计思路

作为一个初学者,我一开始也搞不懂"单例"到底是什么。简单来说,单例模式就是确保一个类在整个应用中只创建一个对象。就像我们班只有一个班长,不会有两个班长一样。

要实现单例模式,无论哪种写法,都离不开三个核心步骤(这是我总结的"万能公式"):

  1. 私有化构造器:禁止外部通过 new 关键字创建对象
  2. 定义静态类变量:存储该类的唯一实例(静态变量属于类级别,不是每个对象都有)
  3. 提供公开静态方法:对外暴露获取唯一实例的入口

二、预加载单例:"类加载时就创建实例"的实现

预加载单例的特点是:类加载时就创建好实例,后续获取直接返回。就像我提前把早餐准备好,早上直接吃就行。

1. 代码实现

package com.wmh.singleinstance;
// 预加载单例类
public class A {
    // 静态变量存储唯一实例:类加载时直接初始化
    private static A a = new A();

    // 私有化构造器:外部无法通过 new 创建对象
    private A() {}

    // 公开静态方法:返回唯一实例
    public static A getInstance() {
        return a;
    }
}

2. 为什么叫"预加载"?

我一开始也很好奇为什么叫"预加载"。后来我理解了:预加载单例在类加载时就创建了实例,不需要等待。这就像我提前把早餐准备好,早上直接吃就行。

3. 优点与缺点

  • 优点:实现简单、天然线程安全(类加载过程本身就是线程安全的)
  • 缺点:如果类加载后长期不使用该实例,会造成内存浪费(比如一个工具类,可能很少用到)

三、延迟初始化单例:"需要时才创建实例"的实现

延迟初始化单例的特点是:只有在第一次获取实例时才创建对象。就像我这种懒人,不到饿得不行,是不想做饭的。

1. 代码实现

package com.wmh.singleinstance;
// 延迟初始化单例类
public class B {
    // 静态变量暂存实例:初始为 null,不提前创建
    private static B b;

    // 私有化构造器:外部无法通过 new 创建对象
    private B() {}

    // 公开静态方法:第一次调用时才创建实例
    public static B getInstance() {
        if (b == null) {
            // 首次获取时创建对象
            b = new B();
        }
        return b;
    }
}

2. 重要提醒:延迟初始化单例不安全!

这是我在学习过程中踩过的大坑!基础版的延迟初始化单例不是线程安全的。如果在多线程环境下,两个线程同时判断 b == null,都可能创建实例,导致多个对象。

四、测试验证:确保实例唯一

我写了一个简单的测试类来验证两种单例是否真的只有一个实例:

1. 测试代码

package com.wmh.singleinstance;
public class Test {
    public static void main(String[] args) {
        // 测试预加载单例
        A a1 = A.getInstance();
        A a2 = A.getInstance();
        System.out.println("预加载单例:a1 == a2 ? " + (a1 == a2));
        System.out.println("a1 地址:" + a1);
        System.out.println("a2 地址:" + a2);

        // 测试延迟初始化单例
        B b1 = B.getInstance();
        B b2 = B.getInstance();
        System.out.println("\n延迟初始化单例:b1 == b2 ? " + (b1 == b2));
        System.out.println("b1 地址:" + b1);
        System.out.println("b2 地址:" + b2);
    }
}

2. 运行结果

预加载单例:a1 == a2 ? true
a1 地址:com.wmh.singleinstance.A@1b6d3586
a2 地址:com.wmh.singleinstance.A@1b6d3586

延迟初始化单例:b1 == b2 ? true
b1 地址:com.wmh.singleinstance.B@4554617c
b2 地址:com.wmh.singleinstance.B@4554617c

3. 我的思考

看到 a1 == a2b1 == b2 都是 true,我松了口气,说明两种实现都达到了单例的目的。但我也明白,如果在多线程环境下,基础版延迟初始化单例可能不安全。

五、单例模式的进阶:枚举类实现单例

在学习过程中,我发现了一个更优雅的单例实现方式——使用枚举类!这在上传的文档中也有提到。

1. 枚举类实现单例

package com.wmh.enumdemo;

public enum SingletonEnum {
    INSTANCE;
    
    // 可以添加一些方法
    public void doSomething() {
        System.out.println("单例枚举方法");
    }
}

2. 为什么枚举类实现单例这么棒?

  1. 天然单例:枚举类本身就是单例的,无需额外处理
  2. 线程安全:枚举类的实例化是线程安全的
  3. 防止反射破坏:枚举类可以防止通过反射创建多个实例
  4. 防止序列化破坏:枚举类可以防止序列化后创建多个实例

3. 使用方式

// 获取单例
SingletonEnum instance = SingletonEnum.INSTANCE;
instance.doSomething();

六、延迟初始化单例线程安全优化方案

基础版延迟初始化单例不安全,但有几种优化方案:

1. 加 synchronized 关键字(简单但效率低)

public static synchronized B getInstance() {
    if (b == null) {
        b = new B();
    }
    return b;
}

2. 双重检查锁定(DCL,推荐)

private static volatile B b;
public static B getInstance() {
    if (b == null) {
        synchronized (B.class) {
            if (b == null) {
                b = new B();
            }
        }
    }
    return b;
}

💡 小提示:volatile 关键字是关键,它可以防止指令重排序,确保在多线程环境下安全。

七、单例模式的应用场景

作为一个初学者,我一开始不太明白单例模式有什么用。后来通过学习,我发现它在实际开发中非常常见:

  • Java中的 Runtime 类Runtime.getRuntime() 返回唯一实例
  • 数据库连接池:避免创建过多连接导致资源耗尽
  • 配置管理类:全局只需一个配置对象
  • 日志管理器:确保日志记录的一致性

八、总结与选择建议

1. 三种单例实现对比

实现方式简单性线程安全优点缺点
预加载单例⭐⭐⭐⭐⭐⭐⭐⭐⭐简单、线程安全可能浪费内存
延迟初始化单例(基础)⭐⭐⭐节省内存不安全
延迟初始化单例(DCL)⭐⭐⭐⭐⭐⭐⭐节省内存、线程安全代码稍复杂
枚举类⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐优雅、安全、防止反射破坏不能延迟加载

2. 我的建议

  • 如果实例创建成本低、且肯定会用到,用预加载单例(简单、安全)
  • 如果实例创建成本高、可能长期不用,用枚举类(最推荐!)
  • 如果必须用延迟初始化单例,一定要用DCL方案(双重检查锁定)

九、我的学习心得

作为一个Java初学者,我最开始被单例模式搞得很懵。后来我总结了几点经验:

  1. 不要在多线程环境下使用基础版延迟初始化单例!这是初学者最容易犯的错误
  2. 枚举类实现单例是最优雅的方式,我强烈推荐在新项目中使用
  3. 理解原理比死记硬背更重要,我花了很多时间理解为什么延迟初始化单例需要双重检查

十、写在最后

单例模式是设计模式的入门经典,掌握它不仅能解决实际开发中的资源控制问题,更能培养"最优解思维"。希望这篇文章能帮助到和我一样的初学者。

如果你也是Java初学者,欢迎在评论区交流:你在项目中用过哪种单例实现?有没有遇到过坑?我特别想听听你的故事!

如果觉得这篇文章对你有帮助,别忘了点赞、收藏、转发给身边的Java小伙伴!😊