Java单例模式分析

简介

单例模式(Singleton Pattern)是一种设计模式,目的是确保一个类只有一个实例,并提供一个全局访问点。‌ 这种模式属于创建型模式,主要用于当某个类在系统中只能存在一个实例时,例如‌配置管理器、‌数据库连接池等场景。‌

单例模式的动机在于确保某些类在系统中只有一个实例。例如,一个系统中只能有一个‌打印任务管理器、‌窗口管理器或‌文件系统等。如果不使用机制对对象进行唯一化,可能会导致多个实例被创建,浪费资源,甚至引起状态不一致的问题。

在Java中,单例模式(Singleton Pattern)是一种常用的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点。

具体单例模式详解

饿汉式(Eager Initialization)

简介

这种方式在类加载时就创建了单例实例,因此它保证了线程安全,并且在类加载时就完成了实例的创建,适用于单例实例需要尽早创建并且不需要延迟加载的场景。

如果用洗碗来做类比的话,吃完饭立刻就去洗碗这个就是属于“饿汉模式”的范畴

饿汉式单例模式主要通过将类的实例初始化放在静态变量中来实现,由于静态变量在类加载时就会初始化,因此这种方式可以保证实例的唯一性。

实现方式

静态成员变量方式

在这种方式中,直接在类中声明一个私有的静态成员变量,并在声明时直接实例化。同时,提供一个公共的静态方法来返回这个实例。构造方法需要被私有化,以防止外部通过 new 关键字创建实例。

示例代码:

public class CommonUtil {
    
    // 私有静态成员变量,类加载时初始化  
    private static final CommonUtil instance = new CommonUtil();
    
    // 私有构造方法,防止外部通过new创建实例  
    private CommonUtil() {}
    
    // 公共静态方法,返回唯一实例
    public static CommonUtil getInstance() {
        return instance;
    }
    public void doSomething() {
        // 操作实例的代码
    }
}

静态代码块方式

虽然静态成员变量方式更为常见,但也可以使用静态代码块来实现相同的效果。在静态代码块中对静态成员变量进行初始化。这种方式在写法上略有不同,但本质上都是在 类加载时完成实例的初始化

示例代码:

public class CommonUtil {

    // 私有静态成员变量  
    private static final CommonUtil instance;
    
    static {  
        // 静态代码块中初始化实例  
        instance = new CommonUtil();  
    }  
    
    // 私有构造方法,防止外部通过new创建实例  
    private CommonUtil() {}
    
    // 公共静态方法,返回唯一实例
    public static CommonUtil getInstance() {
        return instance;
    }
    public void doSomething() {
        // 操作实例的代码
    }

}
优缺点分析

优点:

  1. 线程安全
    • 饿汉式单例在类加载时就完成了实例的创建,因此不存在线程安全问题。它不需要额外的同步措施来保证多线程环境下的安全性。
  2. 简单
    • 实现简单,不需要复杂的逻辑来控制实例的创建。
  3. 性能稳定
    • 由于实例在类加载时就已创建,所以在获取实例时不需要进行任何同步操作,这使得获取实例的性能非常稳定。
  4. 避免延迟加载的复杂性
    • 避免了懒汉式中需要的延迟加载逻辑,简化了代码。

缺点:

  1. 资源浪费
    • 如果创建单例实例是一个重量级的操作(例如涉及复杂的初始化或资源分配),那么即使客户端最终没有使用这个单例,资源也已经分配了,这可能导致资源浪费。
  2. 可能导致内存占用
    • 单例实例在类加载时就创建,这意味着它将一直占用内存,直到JVM停止运行。如果这个单例不是应用程序频繁使用的部分,那么它将无用地占用内存。
  3. 灵活性降低
    • 由于单例实例在类加载时就创建,这限制了单例的一些使用场景,比如需要根据配置文件动态更改单例行为的场景。
  4. 静态初始化块的局限性
    • 如果单例的创建依赖于外部资源(如数据库连接),使用饿汉式可能会有问题,因为静态初始化块不支持延迟初始化或条件初始化。
  5. 序列化问题
    • 饿汉式单例在序列化和反序列化时可能会创建新的实例,这可能导致单例模式被破坏。虽然可以通过实现java.io.Serializable接口并添加一个读对象的方法来解决,但这增加了实现的复杂性。

懒汉式(Lazy Initialization)

简介

这种方式在第一次被调用时才创建单例实例,可以延迟资源的消耗,但是如果多个线程同时调用getInstance()方法,可能会创建多个实例,因此不是线程安全的。

实现方式

默认实现方式-线程不安全

示例代码:

public class CommonUtil {
    
    // 声明私有静态变量,初始化为null 
    private static CommonUtil instance;
    
    // 私有构造方法,防止外部通过new创建实例  
    private CommonUtil() {}
    
    // 提供公共静态方法,返回类的唯一实例
    public static CommonUtil getInstance() {
        // 检查实例是否已创建
        if (instance == null) {
            // 创建实例
            instance = new CommonUtil();
        }
        // 返回实例
        return instance;
    }
    public void doSomething() {
        // 操作实例的代码
    }
}

线程安全

通过synchronized关键字确保线程安全。但是,每次调用getInstance()方法时都会进行同步,这可能会导致性能问题。

示例代码:

public class CommonUtil {
    
    // 声明私有静态变量,初始化为null 
    private static CommonUtil instance;
    
    // 私有构造方法,防止外部通过new创建实例  
    private CommonUtil() {}
    
    // 提供公共静态方法,返回类的唯一实例
    public static CommonUtil getInstance() {
        // 检查实例是否已创建
        if (instance == null) {
            synchronized (CommonUtil.class) {
                // 创建实例
            	instance = new CommonUtil();
            }
        }
        // 返回实例
        return instance;
    }
    public void doSomething() {
        // 操作实例的代码
    }
}

线程安全的优化

双重检查锁定(Double-CheckedLocking)**:通过两次检查实例是否已创建,并在第二次检查时使用 **synchronized 关键字加锁,以减少同步的开销。同时,为了保证跨线程的内存可见性,实例变量需要使用 volatile 关键字修饰。

这种方式是懒汉式的一个变种,它只在实例为null时才进行同步,减少了获取实例时的同步开销,既保证了线程安全,又提高了性能。

示例代码:

public class CommonUtil {
    
    private static volatile CommonUtil instance;
    
    private CommonUtil() {}
    
    public static CommonUtil getInstance() {
        if (instance == null) {
            synchronized (CommonUtil.class) {
                if (instance == null) {
                    instance = new CommonUtil();
                }
            }
        }
        return instance;
    }
    public void doSomething() {
        // 操作实例的代码
    }
}

枚举式(Enum Singleton)

简介

这种方式是通过枚举来实现单例,它不仅能避免多线程问题,还能防止反序列化重新创建新的对象。枚举类型的每个元素都是静态的,因此不需要显式的实例化。、

实现方式

在枚举方式中,CommonUtil.INSTANCE就是单例实例,你可以通过CommonUtil.INSTANCE.doSomething()来访问单例的方法。这种方式简单且线程安全,由JVM从根本上提供保障,是实现单例模式的最佳方法之一。

示例代码:

public enum CommonUtil {
    INSTANCE;
    public void doSomething() {
        // 操作实例的代码
    }
}

总结

单例模式是一种确保特定类只有一个实例,并提供全局访问点的设计模式。它适用于配置管理器、数据库连接池等需要唯一实例的场景。单例模式的实现方式主要有饿汉式、懒汉式和枚举式。

饿汉式在类加载时就创建实例,简单且线程安全,但可能导致资源浪费和内存占用。懒汉式则在第一次调用时创建实例,可以延迟资源消耗,但需要额外的同步措施来保证线程安全,如双重检查锁定。枚举式单例利用Java枚举的特性,实现简单且天然线程安全,还能防止反序列化创建新实例的问题,是实现单例的最佳实践。

每种实现方式都有其适用场景和优缺点。饿汉式适合实例创建成本低、使用频繁的场景;懒汉式适合实例创建成本高、使用频率低的场景;枚举式则适合所有需要单例的场景,特别是需要保证绝对线程安全的情况。选择合适的单例实现方式,可以有效地提高资源利用率和程序性能,同时保证程序的正确性。

欢迎关注公众号:“全栈开发指南针”
这里是技术潮流的风向标,也是你代码旅程的导航仪!
Let’s code and have fun!

你可能感兴趣的:(java,单例模式,后端,架构,面试,学习日记,笔记)