深入理解 Java 单例模式:从基础到最佳实践

发布于:2025-05-01 ⋅ 阅读:(21) ⋅ 点赞:(0)

单例(Singleton)模式是 Java 中最基本、最常用的设计模式之一。它确保一个类在任何情况下都只有一个实例,并提供一个全局访问点来获取这个唯一的实例。

一、为什么需要单例模式?(使用场景)

单例模式主要适用于以下场景:

  1. 资源共享与控制访问:当实例需要共享资源(如数据库连接池、线程池、配置文件、日志对象)或者需要控制对共享资源的并发访问时,单例可以确保所有操作都通过这唯一的实例进行。
  2. 确保唯一实例:某些类在逻辑上只需要一个实例,例如代表应用程序配置的对象、硬件设备管理器等。多实例可能会导致状态不一致或资源冲突。
  3. 懒加载(Lazy Loading):在某些情况下,实例的创建可能比较耗费资源。如果该实例并非立即需要,可以通过懒加载的方式,在首次使用时才创建,从而减少程序启动时的资源消耗。

二、实现单例模式的核心要求

一个标准的单例模式通常包含以下三个要素:

  1. 私有的构造函数:防止外部代码通过 new​ 关键字直接创建类的实例。
  2. 私有的静态实例变量:在类的内部持有该类的唯一实例。
  3. 公有的静态方法:提供一个全局访问点(通常命名为 getInstance()​),用于获取类的唯一实例。

三、常见的单例实现方式

1. 饿汉式(Eager Initialization)

饿汉式在类加载时就直接创建实例,不管后续是否真的用到。

/**
 * 饿汉式单例
 * 优点:实现简单,线程安全(由JVM类加载机制保证)
 * 缺点:非懒加载,可能造成资源浪费
 */
public class EagerSingleton {
    // 1. 私有静态final实例变量,在类加载时就初始化
    private static final EagerSingleton instance = new EagerSingleton();

    // 2. 私有构造方法
    private EagerSingleton() {
        // 防止外部实例化
        System.out.println("EagerSingleton instance created.");
    }

    // 3. 公有静态方法返回实例
    public static EagerSingleton getInstance() {
        return instance;
    }

    public void doSomething() {
        System.out.println("EagerSingleton is doing something.");
    }
}

核心思想:利用 JVM 的类加载机制保证实例创建的线程安全。当类被加载时,静态变量 instance​ 会被初始化,这个过程由 JVM 保证只执行一次。

关键点:

  • 线程安全:天然线程安全,无需额外加锁。
  • 非懒加载:实例在类加载时创建,即使从未调用 getInstance()​。
2. 懒汉式(Lazy Initialization)

懒汉式在首次调用获取实例的方法时才创建实例。需要特别注意线程安全问题。

方式一:简单同步方法(性能较低)

/**
 * 懒汉式单例 - 同步方法
 * 优点:懒加载
 * 缺点:线程安全但性能低(每次调用getInstance都有同步开销)
 */
public class LazySingletonSynchronized {
    private static LazySingletonSynchronized instance;

    private LazySingletonSynchronized() {
        System.out.println("LazySingletonSynchronized instance created.");
    }

    // 使用synchronized保证线程安全,但锁定了整个方法
    public static synchronized LazySingletonSynchronized getInstance() {
        if (instance == null) {
            instance = new LazySingletonSynchronized();
        }
        return instance;
    }
}

方式二:双重校验锁(Double-Checked Locking, DCL)

为了提高性能,避免每次调用 getInstance()​ 都进行同步。

/**
 * 懒汉式单例 - 双重校验锁 (DCL)
 * 优点:懒加载,线程安全,性能相对较高
 * 缺点:实现复杂,需要volatile关键字防止指令重排序
 */
public class LazySingletonDCL {
    // 1. 使用volatile关键字确保可见性和禁止指令重排序
    private static volatile LazySingletonDCL instance;

    private LazySingletonDCL() {
        System.out.println("LazySingletonDCL instance created.");
    }

    public static LazySingletonDCL getInstance() {
        // 第一次检查,避免不必要的同步
        if (instance == null) {
            // 同步块,保证只有一个线程创建实例
            synchronized (LazySingletonDCL.class) {
                // 第二次检查,防止多个线程重复创建
                if (instance == null) {
                    // new操作非原子,volatile防止指令重排序问题
                    instance = new LazySingletonDCL();
                }
            }
        }
        return instance;
    }
}

DCL 中 volatile​ 的重要性:
​instance = new LazySingletonDCL();​ 不是原子操作,可能分为三步:

  1. 分配内存空间。
  2. 初始化对象。
  3. 将 instance​ 指向分配的内存地址。
    JVM 可能进行指令重排序(如 1 -> 3 -> 2)。若无 volatile​,线程 A 执行 1 和 3 后,instance​ 非空但未初始化。线程 B 此时调用 getInstance()​,会跳过同步块直接返回未初始化的 instance​,导致错误。volatile​ 可禁止这种重排序并保证内存可见性。

方式三:静态内部类(推荐的懒汉式)

利用 JVM 类加载机制实现懒加载和线程安全,代码更简洁。

/**
 * 懒汉式单例 - 静态内部类
 * 优点:懒加载,线程安全(由JVM保证),实现简单
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
        System.out.println("StaticInnerClassSingleton instance created.");
    }

    // 静态内部类
    private static class SingletonHolder {
        // 在内部类中持有实例,JVM保证初始化线程安全且只执行一次
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    // 调用getInstance时,才会加载SingletonHolder,从而创建INSTANCE
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

工作原理:

  • 外部类 StaticInnerClassSingleton​ 加载时,静态内部类 SingletonHolder​ 不会被加载。
  • 只有首次调用 getInstance()​ 方法访问 SingletonHolder.INSTANCE​ 时,JVM 才会加载 SingletonHolder​ 类,并初始化 INSTANCE​。
  • JVM 的类加载过程是线程安全的。

初始化时机对比:

特性 静态变量(饿汉式) 静态内部类(懒汉式) 静态方法(懒汉同步方法/DCL)
初始化时机 外部类初始化(加载)阶段 首次主动使用内部类时 首次主动调用 getInstance​ 时

核心:通过 JVM 类加载机制(饿汉式、静态内部类)实现的单例都是天然线程安全的,无需显式加锁。

3. 枚举式(Enum Singleton)—— 最佳实践

《Effective Java》作者 Joshua Bloch 推荐的方式,极其简洁、高效,并且天然防止反射和反序列化攻击。

/**
 * 枚举式单例 - 最佳实践
 * 优点:实现极简,线程安全,防止反射和反序列化攻击
 * 缺点:非传统意义上的懒加载(类加载时实例已备好,但枚举本身加载也可能延迟)
 */
public enum EnumSingleton {
    INSTANCE; // 定义一个枚举元素,它本身就是单例实例

    // 可以添加业务方法
    public void doSomething() {
        System.out.println("EnumSingleton is doing something.");
    }

    // 示例用法
    public static void main(String[] args) {
        EnumSingleton s1 = EnumSingleton.INSTANCE;
        EnumSingleton s2 = EnumSingleton.INSTANCE;
        System.out.println(s1 == s2); // 输出 true
        s1.doSomething();
    }
}

为什么枚举是最佳实践?

  • 简洁性:代码量最少。
  • 线程安全:由 JVM 保证,无需担心。
  • 防止反序列化创建新实例:Java 枚举的序列化机制有特殊处理,readObject()​ 方法会直接返回已存在的枚举常量,而不是创建新对象。
  • 防止反射攻击:反射无法通过 newInstance()​ 方法创建枚举实例(会抛出异常)。
  • 避免资源浪费的误解:虽然枚举实例在类加载时就“准备好了”,但枚举类本身的加载也可能被延迟。更重要的是,枚举的设计意图明确,开发者使用时就是为了获取单例,几乎不存在“加载了但从未被调用”的浪费情况。相比之下,普通的饿汉式如果设计不当(例如被其他无关类意外触发加载),才可能造成实例创建后未被使用的浪费。JVM 对枚举也有内存优化。

四、破坏单例模式的途径及防御

1. 反射(Reflection)

反射可以通过调用私有构造函数来创建新的实例。

破坏示例(假设有 LazySingletonDCL​ 类):

Constructor<LazySingletonDCL> constructor = LazySingletonDCL.class.getDeclaredConstructor();
constructor.setAccessible(true); // 强行访问私有构造
LazySingletonDCL instance1 = LazySingletonDCL.getInstance();
LazySingletonDCL instance2 = constructor.newInstance(); // 创建新实例
System.out.println(instance1 == instance2); // 输出 false

防御:在私有构造函数中增加检查,如果实例已存在,则抛出异常。

private LazySingletonDCL() {
    // 防止反射创建新实例
    if (instance != null) {
        throw new RuntimeException("Singleton instance already exists. Use getInstance() method.");
    }
    System.out.println("LazySingletonDCL instance created.");
}

image

(注意:枚举天然防御反射)

2. 反序列化(Deserialization)

如果单例类实现了 Serializable​ 接口,通过反序列化 readObject()​ 方法会创建一个新的实例。

防御:在单例类中添加 readResolve()​ 方法。该方法在反序列化时被调用,如果存在,其返回值会取代 readObject()​ 返回的对象。我们让它直接返回现有的单例实例。

// 在实现了Serializable的单例类中添加:
protected Object readResolve() {
    return getInstance(); // 返回当前唯一的实例
}

image

(注意:枚举天然防御反序列化)

五、不使用锁实现线程安全的单例

回顾一下,以下方式无需在 getInstance()​ 中使用 synchronized​ 也能保证线程安全:

  1. 饿汉式:利用 JVM 类加载时初始化静态变量的线程安全性。
  2. 静态内部类:利用 JVM 加载内部类时初始化静态变量的线程安全性。
  3. 枚举式:由 JVM 从语言层面保证其唯一性和线程安全性。

六、示例:小明的购物车

下面是你提供的购物车示例代码,它使用了 DCL 实现单例。

import java.util.*;
import java.io.*;

// 使用DCL实现的购物车单例
class ShoppingCart {
    // volatile 保证可见性和禁止指令重排序
    private static volatile ShoppingCart instance;
    // 注意:将购物车内容设为静态,意味着所有用户共享同一个购物车列表!
    // 在真实场景中,购物车通常与用户会话关联,而不是全局单例。
    // 但作为单例模式的演示,这里保持原样。
    private static List<String> productsNames = new ArrayList<>();
    private static List<Integer> produtsQuatities = new ArrayList<>();

    // 私有构造
    private ShoppingCart() {
        System.out.println("ShoppingCart instance created.");
    };

    // DCL 获取实例
    public static ShoppingCart getInstance() {
        if (instance == null) {
            synchronized (ShoppingCart.class) {
                if (instance == null) {
                    instance = new ShoppingCart();
                }
            }
        }
        return instance;
    }

    // 添加商品到共享列表
    public void add(String name, int quantity) {
        productsNames.add(name);
        produtsQuatities.add(quantity);
        System.out.println("Added to cart: " + name + " " + quantity);
    }

    // (可以添加其他方法,如展示购物车内容等)
}

public class Main {
    public static void main(String[] args) {
        // 获取唯一的购物车实例
        ShoppingCart cart = ShoppingCart.getInstance();
        Scanner sc = new Scanner(System.in);
        String inputLine;
        System.out.println("Enter product name and quantity (e.g., 'apple 5'), type 'exit' to quit:");

        while (sc.hasNextLine()) {
            inputLine = sc.nextLine();
            if ("exit".equalsIgnoreCase(inputLine.trim())) {
                break;
            }
            String[] parts = inputLine.trim().split("\\s+"); // 使用正则匹配一个或多个空格
            if (parts.length == 2) {
                String name = parts[0];
                try {
                    int quantity = Integer.parseInt(parts[1]);
                    if (quantity > 0) {
                         // 通过单例实例添加商品
                        cart.add(name, quantity);
                    } else {
                        System.out.println("Quantity must be positive.");
                    }
                } catch (NumberFormatException e) {
                    System.out.println("Invalid quantity format. Please enter a number.");
                }
            } else {
                System.out.println("Invalid input format. Please enter 'name quantity'.");
            }
        }
        sc.close();
        System.out.println("Exiting program.");
        // (可以添加展示最终购物车内容的代码)
    }
}

注意:这个购物车示例将商品列表设为 static​。这意味着无论程序中有多少用户(理论上,即使有多个线程调用 getInstance()​),他们操作的都是同一个商品列表。在真实应用中,购物车通常是每个用户一个实例(可能存放在 Session 中),而不是全局单例。但作为演示单例模式的例子,它展示了如何获取和使用唯一的 ShoppingCart​ 对象。

七、总结

单例模式是保证类只有一个实例的重要工具。选择哪种实现方式取决于具体需求:

  • 追求极致简洁、安全:优先选择 枚举式。
  • 需要懒加载且希望实现简单:静态内部类 是非常好的选择。
  • 需要懒加载且有历史代码或特殊性能考虑:可以使用 DCL,但务必确保 volatile​ 的正确使用。
  • 不介意非懒加载或实例创建成本低:饿汉式 最简单直接。



网站公告

今日签到

点亮在社区的每一天
去签到