Java 实现五种单例模式:饿汉式、懒汉式、双重检查锁定、静态内部类和枚举类

单例模式及 Java 实现

概述

单例模式是一种创建型设计模式,它保证一个类仅有一个实例,并提供一个全局访问点。

实现

单例模式的实现方式

  1. 私有构造函数,禁止外部创建对象。
  2. 定义一个静态私有变量,保存类的唯一实例。
  3. 提供一个静态公共方法,获取类的唯一实例。
public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }
    
    public static Singleton getInstance() {
        return instance;
    }
    
    // other methods...

}

更多的实现方式?

上述的实现模式属于饿汉式实现。之所以叫饿汉式,是因为在类加载时实例就已经被创建。

这种做法的代价就是,如果没有用到该实例,则浪费资源。

事实上,单例模式创建的实例往往体积很大,采用饿汉式实现,类加载时需要耗费大量时间,占用内存资源。

于是有人提出了懒汉式实现等更多的实现方式。

懒汉式实现

懒汉式实现是指,在需要用到实例时,才去创建实例。

这显得很“懒”,但当创建的实例很大时,懒汉式实现可以节省内存资源、提高程序运行效率。

public class Singleton {
    
    private static Singleton instance;

    private Singleton() {
    }
    
    public static Singleton getInstance() {
        if (instance == null) {           // 1
            instance = new Singleton();   // 2
        }
        return instance;
    }
    
    // other methods...
    
}

这样实现的好处是,类加载时并不会创建实例,只有在第一次调用 getInstance() 方法时,才会创建实例。

然而,这种实现方式只有在单线程的情况下才能保证实例的唯一性,多线程环境下单例有可能被破坏。我们来看如下分析。

假设现在有两个线程 A 和 B,都调用 getInstance() 方法。

线程 A 率先进入方法体并执行第 1 行代码,此时实例数量为 0 因此判空为 true 进入第 2 行代码。

此时线程 B 也进入方法体并执行第 1 行代码,此时实例数量为 0 因此判空为 true 进入第 2 行代码。

线程 A 执行第 2 行代码,创建实例并将实例变量 instance 指向该实例,并返回该实例,实例数量为 1。

线程 B 执行第 2 行代码,创建实例并将实例变量 instance 指向该实例,并返回该实例,实例数量为 2。

此时,线程 A 和线程 B 都认为自己创建的实例是唯一的,但实际上 A 线程对实例的修改不会影响到 B 线程的实例。

同步方法懒汉式实现

上述分析表明,多线程情况下会发生线程安全问题,导致实例唯一性被破坏。

为了解决这个问题,我们可以采用同步方法懒汉式的实现方式。

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    // other methods...

}

这样一来,getInstance() 方法就变成了原子操作,线程安全问题解决了。

双重检查锁定懒汉式实现

双重检查锁定是一种优化方案,它在同步方法懒汉式实现的基础上,进一步优化了线程安全问题。

观察上述代码我们发现,当实例已经被唯一创建后,getInstance() 就不需要继续同步了,多个线程就可以同时进入方法体获取实例。

因此,我们可以将 getInstance() 方法修改为双重检查锁定方式。

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            // only creations are synchronized
            createInstance();
        }
        return instance;
    }
    
    private synchronized static void createInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
    }

    // other methods...

}

这里说明一下 volatile 关键字的作用。

  1. 如果一个变量被 volatile 修饰,当多个线程访问该变量时能保证该变量的可见性,即一个线程修改了变量的值,其他线程能立即得知。
  2. 如果一个变量被 volatile 修饰,JVM 将不会进行指令重排序,该变量将不会以半初始化的状态被引用到栈内存中。

createInstance() 方法中,我们将 instance 变量声明为 volatile 以保证单例对象在被赋值前被完整初始化。

使用静态内部类实现

静态内部类也属于懒汉式实现。不同的是,这种方法并不需要在 getInstance 方法体中使用双重检查锁定创建对象,而是巧妙地让对象伴随着静态内部类的加载而被创建。这个静态内部类会被延迟加载,且即便多线程并发调用,类只会加载一次。

我们来看具体的代码。

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }
    
    private static class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = SingletonHolder.INSTANCE;    
        }
        return instance;
    }

    // other methods...

}

Singleton 类被加载时,如果 getInstance 未被调用,静态内部类 SingletonHolder 是不会被加载的。单例不会被创建。

getInstance 被调用时,静态内部类 SingletonHolder 被加载,其静态字段 Singleton INSTANCE 会被创建。

由于静态内部类只会被加载一次,所以对象只会被创建一次,保证了并发场景下的线程安全。

枚举实现

反射和反序列化对单例模式的破坏:

  1. 反射可以绕过单例类的私有构造函数,从而创建出多个实例。

    使用下面的代码就可以通过反射创建出多个实例:

    // 获取 Singleton 类的构造函数
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    
    // 设置构造函数为可访问
    constructor.setAccessible(true);
    
    // 通过反射调用私有构造函数创建新的实例
    Singleton s1 = constructor.newInstance();
    Singleton s2 = constructor.newInstance();
    Singleton s3 = constructor.newInstance();
    
  2. 同样地,如果单例类实现了 Serializable 接口,但没有正确处理反序列化逻辑,就可能导致多个实例被创建。

    运行下面的代码,得到输出结果。

    // 获取单例实例
    Singleton instance1 = Singleton.getInstance();
    System.out.println("Instance 1: " + instance1);
    
    // 序列化单例实例
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"))) {
         oos.writeObject(instance1);
    } catch (IOException e) {
         e.printStackTrace();
    }
    
    // 反序列化单例实例
    Singleton instance2;
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"))) {
         instance2 = (Singleton) ois.readObject();
         System.out.println("Instance 2: " + instance2);
    } catch (IOException | ClassNotFoundException e) {
         e.printStackTrace();
    }
    
    // 比较两个实例
    System.out.println("Instance 1 == Instance 2: " + (instance1 == instance2));
    

    输出结果将类似于:

    Singleton instance created.
    Instance 1: Singleton@6d06d69c
    Instance 2: Singleton@7852e922
    Instance 1 == Instance 2: false
    

在单例模式的实现方式中,枚举实现是一种非常简洁且线程安全的实现方式。它利用了 Java 枚举的特性,天然地保证了单例的唯一性。

枚举实现单例模式是基于 Java 枚举的特性。Java 枚举在运行时只会加载一次,且枚举实例的创建是线程安全的。因此,使用枚举实现单例模式可以避免线程安全问题,同时代码也非常简洁:

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("This is a singleton method.");
    }
}
  1. Java 枚举的实例化是线程安全的,不需要额外的同步机制。
  2. 代码非常简洁,不需要手动管理实例的创建和访问。
  3. 枚举的单例实现可以防止反射攻击,因为 Enum 类会抛出异常来阻止通过反射创建额外的实例。
  4. 枚举的单例实现可以防止通过序列化和反序列化创建多个实例。

虽然枚举实现非常强大,但它也有一些局限性:

  1. 枚举实现的单例模式无法继承其他类(因为枚举本身继承了 java.lang.Enum)。
  2. 枚举实例在类加载时就会被初始化,无法像懒汉式那样延迟初始化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值