1.单例模式(创建型)-灵析社区

菜鸟码转

一、 单例模式(创建型)

  • 单例模式是一种创建型设计模式,它保证特定类仅有一个实例,并且提供一种访问该实例的全局访问点;
  • 单例模式将类的实例化控制在一个单一的点上,以避免多个实例在应用程序中被创建,避免内存浪费和不必要的复杂性;
  • 单例模式通常用作全局配置类或全局对象,可以有效地减少内存使用和防止频繁的对象创建和销毁。

单例模式的应用场景

以下是单例模式的一些应用场景:

  • 配置信息管理器:在一个系统中,有很多地方需要读取配置信息,如果每个地方都创建一遍配置信息管理器,不仅浪费内存,而且还会增加维护成本。使用单例模式可以保证只有一个实例,所有的地方都可以共享这个实例提供的配置信息;
  • 数据库连接池:数据库连接池是数据库连接的缓存池,为了避免频繁创建和释放数据库连接,可以使用单例模式来实现一个全局的数据库连接池;
  • 日志记录器:在一个系统中,需要记录日志的地方可能很多,如果每个地方都创建一个日志记录器,会浪费大量内存。使用单例模式可以确保只有一个实例,所有的地方都可以共享这个实例提供的日志记录功能;
  • 线程池:线程池是多线程编程中经常用到的技术,使用单例模式可以确保只有一个实例,所有的地方都可以共享这个实例提供的线程池服务。
  • 窗口管理器:在一个图形界面程序中,窗口管理器负责管理窗口的创建、销毁、显示和隐藏等操作。使用单例模式可以确保只有一个窗口管理器实例,所有的窗口都可以共享这个实例提供的窗口管理服务。

在单例设计模式中,主要有以下几种常见的写法:饿汉式、懒汉式、静态内部类、枚举。每种写法都有其优缺点,具体应该根据实际需求和场景进行选择。

示例代码

方式一:饿汉式(使用静态变量)

  • 构造器私有化,外部不能创建
  • 本类内部创建对象实例;
  • 提供一个公有的静态方法,返回实例对象。
package cn.leetcode.singleton;

public class Singleton {

    // 私有构造方法,防止外部创建实例
    private Singleton() {
    }

    // 静态实例,在类加载时初始化
    private final static Singleton INSTANCE = new Singleton();

    // 公有静态方法,返回唯一实例
    public static Singleton getInstance(){
        return INSTANCE;
    }

}

还可以在静态代码块中创建单例:

class Singleton {

    private Singleton() {
    }

    private static Singleton INSTANCE;

    static {
        // 在静态代码块中,创建单例对象
        INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

测试方法:

package cn.leetcode.singleton;

public class SingletonTest {

    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance.equals(instance2));
        System.out.println("instance.hashCode=" + instance.hashCode());
        System.out.println("instance2.hashCode=" + instance.hashCode());
    }

}

饿汉式是线程安全的,写法也相对简单,不容易出错。

方式二:懒汉式

提供一个静态的公有方法,当使用到该方法时,才去创建实例,再加入同步处理的代码,解决线程安全问题,即懒汉式。

package cn.leetcode.singleton2;

public class Singleton {

    // 延迟初始化
    private static Singleton INSTANCE = null;

    // 私有构造方法,防止外部创建实例
    private Singleton() {}

    // 公有静态方法,返回唯一实例
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

}

上面的代码看起来没什么问题,但是当多个线程同时调用 getInstance() 方法时,可能会导致多个实例被创建。例如,线程 A 进入 if 条件判断语句块前,线程 B 也进入了该语句块,并创建了一个新的实例。当线程 A 继续执行时,它也会再次创建一个新的实例,这样就会导致多个实例存在。

为了解决这个问题,我们需要在方法内加上 synchronized 关键字,保证同一时刻只有一个线程能够进入方法并创建实例。但是这样会带来性能上的问题,因为每次获取实例都需要获得一个锁,并发性能会受到影响。

为了避免这个问题,我们可以使用双重检查(Double-Checked Locking)的方式,在加锁的前提下再次检查实例是否已经被创建。代码如下:

方式三:双重检查

提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题,同时保证了效率,推荐使用。

package cn.leetcode.singleton3;

public class Singleton {

    // 使用 volatile 关键字确保 instance 变量的可见性、禁止指令重排序
    private static volatile Singleton INSTANCE = null;

    // 私有构造方法,防止外部创建实例
    private Singleton() {
    }

    // 公有静态方法,返回唯一实例
    public static Singleton getInstance() {
        // 第一次检查,不需要加锁
        if (INSTANCE == null) {
            // 加锁
            synchronized (Singleton.class) {
                // 第二次检查,必须加锁
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
    
}

在这个实现中,我们使用了 volatile 关键字来保证 instance 变量的可见性,在多线程环境下可以正确地处理指令重排序等问题。同时,我们在第一次检查实例是否已经创建时不需要加锁,只有在需要创建实例时才进行加锁操作,避免了每次获取实例都需要获得锁的问题,提高了并发性能。

方式四:静态内部类

静态内部类方式是一种比较推荐的单例实现方式,利用 Java 类加载机制保证唯一实例,可以避免线程安全问题,同时也不会出现饿汉式的资源浪费问题。这种方式具体实现起来也比较简单。

package cn.leetcode.singleton4;

public class Singleton {
  
    // 静态内部类,利用 Java 类加载机制保证唯一实例
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    // 私有构造方法,防止外部创建实例
    private Singleton() {
        
    }

    // 公有静态方法,返回唯一实例
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
}

在上面的代码中,Singleton 类的构造方法是私有的,保证外部无法通过 new 关键字创建对象。静态内部类 SingletonHolder 是一个私有的静态内部类,它包含了一个静态常量 INSTANCE,该常量是 Singleton 的唯一实例。当我们调用 getInstance() 方法时,直接返回 SingletonHolder.INSTANCE 即可。

由于静态内部类的特性,它只有在第一次被使用的时候才会被加载和初始化,因此能够保证懒加载和线程安全。另外,由于 SingletonHolder是一个私有的内部类,所以它不会被外部访问到。

总之,相比于双重检查锁定等方式,使用静态内部类实现单例模式更加简洁、安全、并且具有良好的延迟加载能力。

方式五:使用枚举

当使用枚举类型来实现单例模式时,Java 会保证每个枚举常量在 JVM 中只有一个实例。因此,无论在多少次调用中都是同一个对象。这种方式不仅可以避免线程安全问题,也可以防止反射攻击和序列化问题。

enum Singleton {
    // 属性
    INSTANCE;

    public void sayOK() {
        System.out.println("ok");
    }
}

在上面的代码中,Singleton 是一个枚举类型,定义了一个名为 INSTANCE 的枚举常量,它是 Singleton 类的唯一实例。由于枚举类型本身就是单例的,并且在加载枚举类型时就已经创建了 INSTANCE 实例,因此该方法返回的永远都是同一个对象。

我们可以通过 Singleton.INSTANCE 来获取该单例对象,并且可以调用其中的方法。例如:

Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();

总结

单例模式是一种创建型设计模式,它可以确保一个类只有一个实例,并提供全局访问点。单例模式通常被用于管理全局状态以及控制资源的访问。在 Java 中,单例模式有多种实现方式,主要包括:

  • 饿汉式:在类加载时就创建了实例,线程安全,但可能会浪费资源;
  • 懒汉式:在调用获取实例方法时才创建实例,可以节省资源,但需要考虑线程安全问题;
  • 双重检查锁定:在懒汉式的基础上加入双重检查锁定机制,能够解决线程安全问题,同时也避免了每次获取实例都需要获得锁的问题,提高了性能;
  • 静态内部类:利用静态内部类的特性,在外部类加载时不会初始化静态内部类,只有在第一次调用获取实例方法时才初始化静态内部类并创建实例。这种方式既可以保证线程安全,又可以避免饿汉式的资源浪费问题;
  • 枚举:Java 5 开始支持使用枚举类型来实现单例模式,可以防止反射、序列化等情况下的多实例创建。

在实现单例时,需要考虑线程安全、延迟加载、序列化等问题,选择合适的实现方式能够提高程序的可读性、可维护性和性能表现。

常见的面试、笔试问题

单例模式在多线程环境中为什么需要特殊注意?请给出一种线程不安全的单例实现,并说明它的问题所在。

  • 在多线程环境中,单例模式需要特殊注意,因为如果实现不当,就有可能出现线程安全问题。具体来说,在多线程环境下,当多个线程同时调用获取单例对象的方法时,有可能会创建多个实例,从而破坏单例的唯一性;
  • 上面给出的懒汉式单例模式就是一个线程不安全的单例实现;
  • 为了解决这个问题,我们可以使用 synchronized 关键字来保证 getInstance() 方法在同一时刻只能被一个线程访问,从而避免多个线程同时创建实例的情况;
  • 但是使用 synchronized 关键字会影响性能,所以也可以使用双重检查锁定机制来解决线程安全问题。

如何通过枚举类型来实现单例模式?与其他实现方式相比,它有哪些优势?

在 Java 中,我们可以使用枚举类型来实现单例模式。这种方式的实现非常简单,在加载枚举类型时就已经创建了唯一的实例,并且在程序运行期间保证只有一个实例存在。以下是一个使用枚举类型实现单例模式的示例:

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("Do something...");
    }
}

相比于其他实现方式,使用枚举类型实现单例模式有以下优势:

  • 线程安全:枚举类型在 Java 中是线程安全的,因此不需要担心多线程下的并发问题;
  • 避免反射攻击:枚举类型没有公共的构造函数,无法通过反射来创建新的实例,因此能够防止反射攻击;
  • 避免序列化问题:枚举类型默认情况下是不支持序列化的,因此能够避免序列化问题。如果要让枚举类型支持序列化,需要手动添加一个 serialVersionUID 字段,并在枚举类型中实现 readResolve() 方法。这样才能保证枚举类型在序列化和反序列化的过程中都能正确地恢复;
  • 简单易用:使用枚举类型实现单例非常简单,而且代码量非常少,易于理解和维护。

阅读量:2042

点赞量:0

收藏量:0