设计模式(创建型)-单例模式

摘要

在软件开发的世界里,设计模式是开发者们智慧的结晶,它们为解决常见问题提供了经过验证的通用方案。单例模式作为一种基础且常用的设计模式,在许多场景中发挥着关键作用。本文将深入探讨单例模式的定义、实现方式、应用场景以及可能面临的问题与解决方案。

定义

单例模式的核心目标是保证一个类在整个系统中仅有一个实例存在,并且为系统提供一个访问该实例的全局访问点。这种模式在资源管理、数据共享等方面具有重要意义。例如,在一个数据库连接管理系统中,为了避免频繁创建和销毁数据库连接带来的性能开销,使用单例模式确保整个应用程序只有一个数据库连接实例,所有对数据库的操作都通过这个唯一的连接进行。

类图

实现方式

饿汉式

饿汉式单例在类加载时就立即创建唯一的实例对象。代码实现如下:

复制代码
public class HungrySingleton {
    // 声明并初始化唯一实例
    private static final HungrySingleton instance = new HungrySingleton();
    // 私有构造函数,防止外部实例化
    private HungrySingleton() {}
    // 提供全局访问点
    public static HungrySingleton getInstance() {
        return instance;
    }
}

这种方式的优点是线程安全,因为实例在类加载阶段就已创建,而类加载过程由 JVM 保证线程安全。同时,调用效率高,因为不需要额外的同步操作。然而,它的缺点是不能延迟加载。如果该单例对象占用资源较大,而在系统运行过程中可能很长时间都不会用到,那么这种提前创建实例的方式会造成资源浪费。

懒汉式

懒汉式单例是在第一次调用 getInstance 方法时才创建实例。代码如下:

复制代码
public class LazySingleton {
    // 声明静态实例,初始值为null
    private static volatile LazySingleton instance = null;
    // 私有构造函数
    private LazySingleton() {}
    // 同步的获取实例方法
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

懒汉式的优势在于可以延迟加载,只有在真正需要使用实例时才创建,避免了资源的过早占用。但是,由于 getInstance 方法使用了 synchronized 关键字进行同步,在多线程环境下,每次调用该方法都需要进行同步操作,这会导致调用效率不高。

双重检查锁式

双重检查锁模式旨在解决单例、性能和线程安全问题。代码如下:

复制代码
public class LazyMan3 {
    private LazyMan3(){}
    private static volatile LazyMan3 instance;
    public static LazyMan3 getInstance(){
        //第一次判断,如果instance不为null,不需要抢占锁,直接返回对象
        if (instance == null){
            synchronized (LazyMan3.class){
                //第二次判断
                if (instance == null){
                    instance = new LazyMan3();
                }
            }
        }
        return instance;
    }
}
class LazyMan3Test{
    public static void main(String[] args) {
        LazyMan3 instance = LazyMan3.getInstance();
        LazyMan3 instance1 = LazyMan3.getInstance();
        System.out.println(instance == instance1);
    }
}

这种方式通过两次 if 判断,在第一次判断实例不为 null 时,直接返回实例,避免了同步操作带来的性能开销。只有当实例为 null 时,才进入同步块进行实例创建。然而,在多线程环境下,由于 JVM 会对实例化对象进行优化和指令重排序操作,可能会出现空指针问题。解决这个问题的方法是使用 volatile 关键字修饰 instance 变量,volatile 可以保证可见性和有序性,防止指令重排序导致的空指针异常。

静态内部类式

静态内部类式单例利用了类加载机制来实现线程安全和延迟加载。代码如下:

复制代码
public class LazyMan4 {
    private LazyMan4(){}
    //定义一个静态内部类
    private static class LazyMan4Holder{
        private static final LazyMan4 INSYANCE = new LazyMan4();
    }
    //对外访问方法
    public static LazyMan4 getInstance(){
        return LazyMan4Holder.INSYANCE;
    }
}
class LazyMan4Test{
    public static void main(String[] args) {
        LazyMan4 instance = LazyMan4.getInstance();
        LazyMan4 instance1 = LazyMan4.getInstance();
        System.out.println(instance == instance1);
    }
}

当外部类 LazyMan4 被加载时,其静态内部类 LazyMan4Holder 并不会立即被加载。只有当调用 getInstance 方法时,LazyMan4Holder 才会被加载,此时会创建 LazyMan4 的唯一实例。这种方式保证了线程安全,因为类加载过程是线程安全的。同时,实现了延迟加载,提高了资源的利用效率。不过,与懒汉式类似,其调用效率相对不高。

静态代码块式

静态代码块式单例在静态代码块中完成实例的初始化。代码如下:

复制代码
public class HungryChinese2 {
    //私有构造方法,为了不让外界创建该类的对象
    private HungryChinese2(){}
 
    //声明该类类型的变量
    private static HungryChinese2 hungryChinese2;//初始值为null
 
    //静态代码块中赋值
    static {
        hungryChinese2 = new HungryChinese2();
    }
 
    //对外提供的访问方式
    public static HungryChinese2 getInstance(){
        return hungryChinese2;
    }
}
class HungryChinese2Test{
    public static void main(String[] args) {
        HungryChinese2 instance = HungryChinese2.getInstance();
        HungryChinese2 instance1 = HungryChinese2.getInstance();
        System.out.println(instance.equals(instance1));
    }
}

这种方式与饿汉式类似,在类加载时通过静态代码块创建实例,因此线程安全,但不能延迟加载。

枚举式

枚举式单例是一种简洁且强大的实现方式。代码如下:

复制代码
public enum LazyMan5 {
    INSTANCE;
}
class LazyMan5Test{
    public static void main(String[] args) {
        LazyMan5 instance = LazyMan5.INSTANCE;
        LazyMan5 instance1 = LazyMan5.INSTANCE;
        System.out.println(instance == instance1);
    }
}

使用枚举实现单例,不仅线程安全,调用效率高,而且天然地防止了反射和反序列化漏洞。在反序列化时,枚举类型会保证返回的是已有的枚举常量,而不会创建新的对象。不过,它同样不能延迟加载。

应用场景

  1. 资源管理:如数据库连接池、线程池等资源,使用单例模式可以确保整个系统中只有一个资源实例,避免资源的重复创建和浪费,提高资源的利用率和管理效率。

  2. 全局配置:系统的全局配置信息,如系统参数、环境变量等,使用单例模式可以方便地在整个系统中访问和修改这些配置,保证配置的一致性。

  3. 日志记录:日志记录器通常使用单例模式,以便在整个应用程序中记录日志信息。所有的日志记录操作都通过同一个日志记录器实例进行,方便管理和维护日志文件。

  4. 缓存管理:缓存系统可以使用单例模式来管理缓存实例,确保不同模块对缓存的访问和操作是一致的,提高缓存的命中率和性能。

单例模式可能面临的问题及解决方案

Serializable问题

  • 如果单例类实现了 java.io.Serializable 接口,在反序列化时可能会出现问题。因为反序列化过程会创建一个新的对象,这可能导致多次反序列化同一对象时得到多个单例类的实例,破坏了单例模式的唯一性。解决方法是在单例类中添加 readResolve 方法:

    private Object readResolve() throws ObjectStreamException {
    return instance;
    }

这样,在反序列化时,如果定义了 readResolve 方法,则直接返回此方法指定的对象,而不会创建新的对象,从而保证了单例的唯一性。

在Android 中使用单例模式可能会内存泄漏

在 Android 开发中,当单例类依赖于 Context 时,如果传入的是 ActivityContext,可能会导致内存泄漏。例如:

复制代码
public class CommUtils {

    private volatile static CommUtils mCommUtils;

    private Context mContext;
    public CommUtils(Context context) {
        mContext=context;
    }

    public static  CommUtils getInstance(Context context) {
        if (mCommUtils == null) {
            synchronized (CommUtils.class) {
                if (mCommUtils == null) {
                    mCommUtils = new CommUtils(context);
                }
            }
        }
        return mCommUtils;
    }
}

只要这个单例没有被释放,那么持有该单例的 Activity 也不会被释放,直到进程退出。为了解决这个问题,应尽量使用 ApplicationContext,因为 Application 的生命周期伴随着整个进程的周期,不会因为某个 Activity 的销毁而导致单例持有无效的 Context,从而避免内存泄漏。

总结

单例模式在软件开发中具有广泛的应用,不同的实现方式各有优劣。开发者需要根据具体的需求和场景,选择合适的单例实现方式,同时注意解决可能出现的问题,以确保系统的高效、稳定运行。通过合理运用单例模式,可以提高代码的可维护性、可扩展性和性能,为软件项目的成功开发奠定坚实的基础。

相关推荐
杨充4 小时前
07.迪米特原则介绍
设计模式
码农的天塌了8 小时前
Java 设计模式之享元模式(Flyweight Pattern)
java·设计模式·享元模式
11在上班9 小时前
剖析initData在水合中的设计哲学
前端·设计模式
用户86178277365189 小时前
责任链
java·后端·设计模式
hope_wisdom9 小时前
实战设计模式之解释器模式
设计模式·解释器模式·软件工程·软件构建·架构设计
誰能久伴不乏13 小时前
单例模式(Singleton Pattern)详解:确保类的唯一性
单例模式
搞不懂语言的程序员1 天前
单例模式详解(java)
java·开发语言·单例模式
隔壁小查1 天前
【多线程】单例模式
单例模式
开发者工具分享1 天前
微服务架构中10个常用的设计模式
微服务·设计模式·架构