Java 中单例模式的正确用法?

本文介绍了Java中单例模式的五种实现方式:饿汉式、懒汉式、双重检查式、静态内部类和枚举式。讨论了各种方式的优缺点,特别强调了线程安全、延迟加载和防止反序列化破坏单例的重要性。枚举写法因其简洁、线程安全和防止反序列化破坏的优势被推荐使用。

单例模式指的是,保证一个类只有一个实例,并且提供一个全局可以访问的入口。

那么我们为什么需要单例呢,其中一个理由,那就是为了节省内存、节省计算。很多情况下,我们只需要一个实例就够了,如果出现了更多的实例,反而属于浪费。举个例子:

public class ExpensiveResource {
    public ExpensiveResource() {
        field1 = // 查询数据库
        field2 = // 然后对查到的数据做大量计算
        field3 = // 加密、压缩等耗时操作
    }
}

这个类在构造的时候,需要查询数据库并对查到的数据做大量计算,所以在第一次构造时,我们花了很多时间来初始化这个对象。但是假设我们数据库里的数据是不变的,并把这个对象保存在了内存中,那么以后就用同一个实例了,如果每次都重新生成新的实例,实在是没必要。

第二个理由就是为了保证结果的正确性,比如我们需要一个全局的计数器,如果有多个实例就会造成混乱了。

适用场景

无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象。

全局信息类:比如我们在一个类上记录网站的访问次数,并且不希望有的访问被记录在对象 A 上,有的却被记录在对象 B 上,这时候我们就可以让这个类成为单例,需要计数的时候拿出来用即可。

常见的写法有 5 种:饿汉式、懒汉式、双重检查式、静态内部类式、枚举式。

饿汉式

//饿汉式
public class Singleton {
 
    private static Singleton singleton = new Singleton();
 
    private Singleton(){}
 
    public static Singleton getInstance(){
        return singleton;
    }
}

用 static 修饰我们的实例,并把构造函数用 private 修饰。这是最直观的写法。由 JVM 的类加载机制保证了线程安全。

这种写法的缺点也比较明显,那就是在类被加载时便会把实例生成出来,所以假设我们最终没有使用到这个实例的话,便会造成不必要的开销。

下面我们再来看下饿汉式的变种——静态代码块形式。缺点和第一种写法一样。

public class Singleton {
 
    private static Singleton singleton;
 
    static {
        singleton = new Singleton();
    }
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return singleton;
    }
}

懒汉式

public class Singleton {
 
    private static Singleton singleton;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

这种写法的优点在于,只有在 getInstance 方法被调用的时候,才会去进行实例化,所以不会造成资源浪费,但是在创建的过程中,并没有考虑到线程安全问题,如果有两个线程同时执行 getInstance 方法,就可能会创建多个实例。所以这里需要注意,不能使用这种方式,这是错误的写法。

为了避免发生线程安全问题,我们可以对前面的写法进行升级,那么线程安全的懒汉式的写法是怎样的呢。


public class Singleton {
 
    private static Singleton singleton;
 
    private Singleton() {}
 
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

我们在 getInstance 方法上加了 synchronized 关键字,保证同一时刻最多只有一个线程能执行该方法,这样就解决了线程安全问题。但是这种写法的缺点也很明显:如果有多个线程同时获取实例,那他们不得不进行排队,多个线程不能同时访问,然而这在大多数情况下是没有必要的。

为了提高效率,缩小同步范围,就把 synchronized 关键字从方法上移除了,然后再把 synchronized 关键字放到了我们的方法内部,采用代码块的形式来保护线程安全。

public class Singleton {
 
    private static Singleton singleton;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

这种写法是错误的。它的本意是想缩小同步的范围,但是从实际效果来看反而得不偿失。因为假设有多个线程同时通过了 if 判断,那么依然会产生多个实例,这就破坏了单例模式。

所以,为了解决这个问题,在这基础上就有了“双重检查模式”。

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

这种写法的优点就是不仅做到了延迟加载,而且是线程安全的,同时也避免了过多的同步环节。

静态内部类

public class Singleton {
 
    private Singleton() {}
 
    private static class SingletonInstance {
        private static final Singleton singleton = new Singleton();
    }
 
    public static Singleton getInstance() {
        return SingletonInstance.singleton;
    }
}

相比于饿汉式在类加载时就完成实例化,这种静态内部类的写法并不会有这个问题,这种写法只有在调用 getInstance 方法时,才会进一步完成内部类的 singleton 的实例化,所以不存在内存浪费的问题。

这里简单做个小总结,静态内部类写法与双重检查模式的优点一样,都是避免了线程不安全的问题,并且延迟加载,效率高。

可以看出,静态内部类和双重检查的写法都是不错的写法,但是它们不能防止被反序列化生成多个实例,那有没有更好的写法呢?最后我们来看枚举方式的写法。

/**
 * 描述: 枚举式单例的写法
 */
public enum Singleton {
    INSTANCE;

    public void whatever() {
        System.out.println("执行了单例类的方法,例如返回环境变量信息");
    }
    public static void main(String[] args) {
        //演示如何使用枚举写法的单例类
        Singleton.INSTANCE.whatever();
    }
}

枚举写法的优点:

首先就是写法简单。枚举的写法不需要我们自己考虑懒加载、线程安全等问题。同时,代码也比较“短小精悍”,比任何其他的写法都更简洁,很优雅。

第二个优点是线程安全有保障,枚举类的本质也是一个 Java 类,但是它的枚举值会在枚举类被加载时完成初始化,所以依然是由 JVM 帮我们保证了线程安全。

前面几种实现单例的方式,其实是存在隐患的,那就是可能被反序列化生成新对象,产生多个实例,从而破坏了单例模式。接下来要说的枚举写法的第 3 个优点,它恰恰解决了这些问题。

对 Java 官方文档中的相关规定翻译如下:“枚举常量的序列化方式不同于普通的可序列化或可外部化对象。枚举常量的序列化形式仅由其名称组成;该常量的字段值不存在于表单中。要序列化枚举常量,ObjectOutputStream 将写入枚举常量的 name 方法返回的值。要反序列化枚举常量,ObjectInputStream 从流中读取常量名称;然后,通过调用 java.lang.Enum.valueOf 方法获得反序列化常量,并将常量的枚举类型和收到的常量名称作为参数传递。”

也就是说,对于枚举类而言,反序列化的时候,会根据名字来找到对应的枚举对象,而不是创建新的对象,所以这就防止了反序列化导致的单例破坏问题的出现。

对于通过反射破坏单例而言,枚举类同样有防御措施。反射在通过 newInstance 创建对象时,会检查这个类是否是枚举类,如果是,就抛出 IllegalArgumentException(“Cannot reflectively create enum objects”) 异常,反射创建对象失败。

可以看出,枚举这种方式,能够防止序列化和反射破坏单例,在这一点上,与其他的实现方式比,有很大的优势。安全问题不容小视,一旦生成了多个实例,单例模式就彻底没用了。

所以结合讲到的这 3 个优点,写法简单、线程安全、防止反序列化和反射破坏单例,枚举写法最终胜出。

<think>好的,用户现在想了解单例模式在设计模式中的意义和用途。我需要先回顾之前的对话,确保回答的一致性和连贯性。用户之前询问了C++运算符重载的意义,现在转向设计模式中的单例模式,可能是在学习设计模式的基础知识,或者是在项目中遇到了相关问题。 首先,用户的当前问题明确:单例模式的意义和用途。我需要结合提供的四个引用内容来组织回答。引用1和2提到了单例模式的核心定义,即确保一个类只有一个实例,并提供全局访问点。引用3和4分别讨论了饿汉模式和懒汉模式,以及JavaScript中的单例实现。 接下来,我需要确定回答的结构。根据用户之前的回答格式,他们喜欢分点阐述,清晰的结构,比如基本概念、核心意义、应用场景、实现方式、注意事项等。同时,用户希望生成相关问题,所以最后要添加相关问题部分。 需要注意引用来源的标注。用户提供的引用[1]到[4]需要适当融入回答中,但不要直接复制,而是用自己的话总结,并在适当位置添加引用标记。例如,引用[1]和[2]都提到了单例模式的应用场景,如任务管理器,文件操作,这些可以作为例子。引用[3]中的饿汉模式和懒汉模式需要解释,但用户可能不需要代码示例,不过可能还是需要简要说明两者的区别。引用[4]提到JavaScript中的全局变量作为单例的例子,这一点可以作为不同语言中的实现方式来提及。 用户可能的需求是理解单例模式为什么重要,何时使用它,以及如何正确应用。需要强调单例模式解决的问题:控制实例数量,避免资源浪费,保证一致性。同时,要提到可能的误用,比如过度使用导致全局状态难以管理,或者引入不必要的耦合。 在组织内容时,可能需要分几个部分:定义、意义、应用场景、实现方式、注意事项。每个部分下用简洁的条目,配合例子和引用。例如,在应用场景中可以举数据库连接池、配置管理、日志记录器等例子,引用[2]中的任务管理器。在实现方式中区分饿汉和懒汉,引用[3]的代码示例。 需要确保语言口语化,避免过于技术化的术语,但保持专业性。同时,检查是否有必要添加更多例子,帮助用户理解。比如,引用[4]的JavaScript例子,可以说明单例在不同语言中的普遍性。 最后,生成的相关问题要针对用户可能进一步想了解的内容,比如如何实现线程安全的单例,单例模式的优缺点,与其他设计模式的对比等。确保问题覆盖用户可能的后续疑问。 总结步骤: 1. 定义单例模式,引用提供的资料。 2. 阐述其核心意义,如资源控制、全局访问。 3. 应用场景,结合例子。 4. 实现方式,区分饿汉和懒汉。 5. 注意事项,如线程安全、过度使用的问题。 6. 相关问题部分,生成四个相关问题。</think>### 单例模式的意义与用途 #### 一、基本定义 单例模式(Singleton Pattern)是设计模式中的 **创建型模式**,其核心目标是 **确保一个类仅有一个实例**,并提供该实例的 **全局访问点**。这一设计模式通过限制类的实例化次数,避免重复创建对象,从而优化资源使用并保证系统行为的一致性[^1][^2]。 --- #### 二、核心意义 1. **资源控制与效率优化** 单例模式通过单一实例减少重复对象的创建,节省内存和计算资源。例如,数据库连接池若频繁创建和销毁连接会极大降低性能,单例模式可统一管理连接[^2]。 2. **全局唯一性保证** 某些场景需要严格限制实例数量,如任务管理器、日志记录器或配置管理器。单例模式确保这些关键组件全局唯一,避免多实例导致的数据冲突或状态混乱[^1][^4]。 3. **简化全局访问** 通过静态方法(如 `getInstance()`)提供全局访问入口,无需依赖对象传递,降低代码耦合度。例如,配置参数可在任何模块中直接调用 `ConfigManager.getInstance()` 获取[^3]。 --- #### 三、典型应用场景 1. **共享资源管理** - 数据库连接池、线程池等需全局共享的资源[^2]。 - 文件系统操作(如同一时间仅允许一个进程写入文件)。 2. **配置与状态管理** - 应用程序的全局配置(如主题、语言设置)。 - 用户登录状态或权限管理。 3. **工具类与公共服务** - 日志记录器(所有模块共享同一日志实例)。 - 缓存服务(统一管理缓存策略)。 --- #### 四、实现方式与示例 单例模式主要有两种实现思路: 1. **饿汉模式(Eager Initialization)** 在类加载时直接创建实例,线程安全但可能提前占用资源。 ```java public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} // 私有构造方法 public static Singleton getInstance() { return instance; } } ``` **适用场景**:实例创建简单且程序启动后需立即使用[^3]。 2. **懒汉模式(Lazy Initialization)** 延迟实例化,首次调用 `getInstance()` 时创建对象,需处理多线程安全问题。 ```java public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` **适用场景**:实例初始化耗时或资源敏感的场景[^3]。 3. **其他变体** - 双重检查锁(Double-Checked Locking):优化懒汉模式的性能。 - 静态内部类:结合饿汉模式的线程安全与懒加载特性。 - 枚举单例:Java 中防止反射攻击的推荐方式。 --- #### 五、注意事项 1. **线程安全** 多线程环境下需通过同步机制(如 `synchronized`)或原子操作确保唯一性。 2. **避免过度使用** 单例模式本质是全局状态,滥用可能导致代码耦合度高、测试困难。 3. **防止反射与序列化破坏** 某些语言(如 Java)需额外处理反射调用构造方法或序列化生成新实例的问题。 4. **跨语言特性** 单例模式在不同语言中实现方式不同。例如,JavaScript 中全局变量 `let config = {}` 可视为单例[^4]。 --- ### 总结 单例模式通过 **控制实例数量** 和 **提供统一访问入口**,解决了资源共享、全局状态管理等核心问题。它在系统关键组件中扮演重要角色,但需谨慎使用以避免设计僵化。 --- ### 相关问题 1. 如何实现线程安全的懒汉模式(Lazy Initialization)? 2. 单例模式在分布式系统中是否仍然有效? 3. 单例模式与静态类的区别是什么? 4. 为什么枚举单例在 Java 中被认为是安全的? [^1]: 单例模式的定义与典型应用场景 [^2]: 单例模式在资源管理中的作用 [^3]: 饿汉模式与懒汉模式的实现对比 [^4]: JavaScript 中的单例模式实现
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值