单例模式(Singleton Pattern)

发布于:2025-09-03 ⋅ 阅读:(21) ⋅ 点赞:(0)

单例概述

单例是需要在内存中永远只能创建一个类的实例

单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。

单例模式的适用场景:

  • 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
  • 无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。

单例模式有8种

单例模式我们可以提供出8种写法,有很多时候我们存在饿汉式单例的概念,以及懒汉式单例的概念。

饿汉式单例的含义是:在获取单例对象之前对象已经创建完成了。

懒汉式单例是指:在真正需要单例的时候才创建出该对象。


饿汉单例的2种写法

特点:在获取单例对象之前对象已经创建完成了。

饿汉式(静态常量)

/**
 * 饿汉式(静态常量)
 */
public class Singleton1 {
    private static final Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }

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

饿汉式(静态代码块)

/**
 * 饿汉式(静态代码块)(可用)
 */
public class Singleton2 {
    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2() {
    }

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

懒汉式单例4种写法

特点:在真正需要单例的时候才创建出该对象。在Java程序中,有时候可能需要推迟一些高开销对象的初始化操作, 并且只有在使用这些对象的时候才初始化,此时,程序员可能会采用延迟初始化。

值得注意的是:要正确的实现线程安全的延迟初始化还是需要一些技巧的,否则很容易出现问题。

懒汉式(线程不安全)

/**
 * 描述: 懒汉式(线程不安全,不推荐的方案)
 */
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {
    }

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

懒汉式(线程安全,性能差)

分析

使用synchronized关键字修饰方法包装线程安全,但性能差多,并发下只能有一个线程正在进入获取单例对象。

案例

/**
 * 描述: 懒汉式(线程安全 ,性能差,不推荐)
 */
public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {
    }

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

懒汉式(线程不安全)

特点:是一种优化后的似乎线程安全的机制。

/**
 * 描述: 懒汉式(线程不安全 ,不推荐)
 */
public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        // 性能得到了优化,但是依然不能保证第一次获取对象的线程安全!
        if (instance == null) {
            // A , B
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

懒汉式(volatile双重检查模式,推荐)

案例代码

/**
 * 描述: 双重检查,推荐面试中进行使用。
 */
public class Singleton6 {
    // 静态属性,volatile保证可见性和禁止指令重排序
    private volatile static Singleton6 instance = null;// 私有化构造器

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        // 第一重检查锁定
        if (instance == null) {
            // 同步锁定代码块
            synchronized (Singleton6.class) {
                // 第二重检查锁定
                if (instance == null) {
                    // 注意:非原子操作
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}

分析

双重检查的优点:线程安全,延迟加载,效率较高!

以上是否就可以了呢,答案是否定的,实际上我们还需要加上volatile修饰,为何要使用volatile保证安全?

1、禁止指令重排序

对象实际上创建对象要进过如下几个步骤

a. 分配内存空间。
b. 调用构造器, 初始化实例。
c. 返回地址给引用

所以,new Singleton()是一个非原子操作,编译器可能会重排序【构造函数可能在整个对象初始化完成前执行 完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的引用)在初始化对象前完成】。而线程B在线程A赋值完时判断instance就不为null了,此时B拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程B会继续拿着个没有初始化的对象中的数据进行操作,此时容易触发“NPE异常”

图解如下

在这里插入图片描述

2、保证可见性。

  • 由于可见性问题,线程A在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程B在主存中判断 instance还是null,那么线程B又将在自己的工作线程中创建一个实例,这样就创建了多个实例。
  • 如果加上了volatile修饰instance之后,保证了可见性,一旦线程A返回了实例,线程B可以立即发现Instance不为null。

静态内部类单例方式

引入:JVM在类初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期 间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另一种线程安全的延迟初始化方案

/**
 * 描述:静态内部类方式,可用
 */
public class Singleton7 {
    private Singleton7() {
    }

    private static class SingletonInstance {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    // 线程 A 线程B
    public static Singleton7 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

小结

  1. 静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上 JVM的特性,这种方式又实现了线程安全的创建单例对象。
  2. 通过对比基于volatile的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更简洁。但是基于volatile的双重检查锁定方案有一个额外的优势除了可以对静态字段实现延迟加载初始化外, 还可以对实例字段实现延迟初始化。

枚举实现单例

/**
 * 描述: 枚举单例
 */
public enum Singleton8 {
    INSTANCE;

    public void whateverMethod() {
    }
} 

**上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。**为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,双重锁校验还是有问题的,因为他无法解决反射和反序列化会破坏单例的问题。

枚举可解决线程安全问题

上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?

其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

那么,“底层”到底指的是什么?

定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

而且,枚举中的各个枚举项同事通过static来定义的。如:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}

反编译后代码为:

public final class T extends Enum
{
    //省略部分内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

枚举可避免反射破坏单例

双重锁校验被反射破坏示例代码

import java.lang.reflect.Constructor;

public class BreakSingleton {
    public static void main(String[] args) throws Exception {
        // 1. 正常获取单例实例
        Singleton6 s1 = Singleton6.getInstance();
        
        // 2. 反射破坏流程
        Constructor<Singleton6> constructor = Singleton6.class.getDeclaredConstructor();
        constructor.setAccessible(true); // 强制访问私有构造器
        Singleton6 s2 = constructor.newInstance();
        Singleton6 s3 = constructor.newInstance();

        // 3. 验证结果
        System.out.println("s1 == s2? " + (s1 == s2)); // false
        System.out.println("s2 == s3? " + (s2 == s3)); // false
    }
}

枚举可避免反射破坏单例原理

  1. JVM底层拦截
    反射调用newInstance()创建对象时,JDK会检查目标类是否ENUM修饰(源码级硬编码拦截),直接抛出异常阻止实例化。

  2. 枚举本质特殊类
    每个枚举值本质是public static final常量,在类加载阶段由JVM原子初始化(线程安全),不存在空实例期,从根源消除反射破坏入口。

核心:Java语言规范(JLS 8.9)明确禁止反射操作枚举类构造器。

枚举可避免反序列化破坏单例

双重锁校验被反序列化破坏示例代码

// 实现序列化接口(破坏前提)
public class Singleton6 implements Serializable {
    // ... 原双重检查代码不变
}

// 测试类
public class BreakDemo {
    public static void main(String[] args) throws Exception {
        Singleton6 s1 = Singleton6.getInstance();
        
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(s1);
        oos.close();
        
        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        Singleton6 s2 = (Singleton6) ois.readObject(); // 破坏!
        
        System.out.println(s1 == s2); // false (单例被破坏)
    }
}

解决方法:

添加 readResolve() 方法拦截反序列化:

public class Singleton6 implements Serializable {
    // ... 原代码不变

    // 反序列化防护盾
    private Object readResolve() {
        return getInstance(); // 始终返回真实单例
    }
}

枚举可避免反序列破坏单例原理

  1. 序列化仅存储枚举名
    枚举序列化时只保存枚举常量名称(如 INSTANCE),不存储对象状态信息。

  2. 反序列化执行valueOf重建
    反序列化时直接调用:

    Enum.valueOf(Singleton.class, "INSTANCE"); // JVM内置逻辑
    

    等同于显式调用 Singleton.INSTANCE永远返回唯一实例

  3. 底层强制拦截新实例创建
    ObjectInputStream 源码硬编码判断:

    if (cl.isEnum()) {
        return Enum.valueOf((Class)cl, name); // 禁止新建对象
    }
    

网站公告

今日签到

点亮在社区的每一天
去签到