简谈设计模式之单例模式

上一篇博客已经介绍了设计模式及其设计原则, 在这篇博客中笔者会介绍一下单例模式, 也是最简单的一种设计模式

单例模式

单例模式属于创建型模式. 它涉及到一个单一的类, 该类负责创建自己的对象, 同时确保只有单个对象被创建, 这个类提供了一种访问其唯一对象的方式, 可以直接访问, 不需要实例化这个类的对象

单例模式结构

  • 单例类. 只能创建一个实例的类
  • 访问类. 使用单例类

单例模式实现

  1. 饿汉式单例

特点: 在类加载时就创建实例, 线程安全, 但是可能会导致资源浪费

java 复制代码
public class Singleton {
    // 在类加载时就创建实例instance
    private static final Singleton instance = new Singleton();
    
    // 私有的构造函数, 避免从外部构造新实例
    private Singleton() {}
    
    // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance
    public static Singleton getInstance() {
        return instance;
    }
}
  1. 懒汉式单例

特点: 延迟创建实例, 但是线程不安全

java 复制代码
public class Singleton {
    // 单个的实例
    private static Singleton instance;
    
    // 私有的构造函数, 避免从外部构造新实例
    private Singleton() {}
    
    // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance
    public static Singleton getInstance() {
        // 假如这时instance还没有被创建, 那么就创建一个新的实例instance
        if (instace == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式单例模式在多线程环境下容易导致线程不安全, 这是因为多个线程可能会同时访问 getInstance() 方法并且同时进入 if (instance == null) 代码块, 这样就会创建多个实例, 违背了单例模式的原则.

  1. 线程安全的懒汉式单例

特点: 延迟创建实例, 使用同步方法保证线程安全, 但是会有性能开销

java 复制代码
public class Singleton {
    // 单个的实例
    private static Singleton instance;
    
    // 私有的构造函数, 避免从外部构造新实例
    private Singleton() {}
    
    // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance, 使用同步方法保证线程安全
    public static synchronized Singleton getInstance() {
        // 假如这时instance还没有被创建, 那么就创建一个新的实例instance
        if (instace == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. 双重检查锁

特点: 提高性能, 减少同步开销, 线程安全

java 复制代码
public class Singleton {
    // 单个的实例
    private static Singleton instance;
    
    // 私有的构造函数, 避免从外部构造新实例
    private Singleton() {}
    
    // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance
    public static Singleton getInstance() {
        // 第一次判断实例是否为null, 如果不为null就直接返回实例, 不进入抢锁阶段
        if (instance == null) {
            synchronized (Singleton.class) {
                // 抢到锁了再判断一次是否为null
            	if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

双重检查锁模式可能会出现空指针问题, 出现问题的原因是JVM在实例对象时会进行优化和指令重排序操作

为了解决空指针异常问题, 可以使用 volatile 关键字, volatile 关键字可以保证可见性和有序性

java 复制代码
public class Singleton {
    // 单个的实例, 使用volatile关键字保证其可见性和有序性
    private static volatile Singleton instance;
    
    // 私有的构造函数, 避免从外部构造新实例
    private Singleton() {}
    
    // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance
    public static Singleton getInstance() {
        // 第一次判断实例是否为null, 如果不为null就直接返回实例, 不进入抢锁阶段
        if (instance == null) {
            synchronized (Singleton.class) {
                // 抢到锁了再判断一次是否为null
            	if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

笔者写到这一段的时候突然想到, 如果把上面双重检查锁的代码略改一下, 改成下面这样, 是否可行?

java 复制代码
// Double-Checked Locking version 1
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            instance = new Singleton();
        }
    }
    return instance;
}
//======================================
// Double-Checked Locking version 2
public static Singleton getInstance() {
    synchronized (Singleton.class) {
        if (instance == null) {
            instance = new Singleton();
        }
    }
    return instance;
}

上面的两种改法, 分别是把synchronized同步块内和同步块外的判断语句 if (instance == null) 删掉之后得到的新代码.

上面这两种改法是否可行呢? 其实都不好. 对于版本1, 假设有线程1和线程2, 进行了如下操作

复制代码
---------------------------------------------------------
        Thread 1                    Thread 2
           |                            |
           |                            |
           |                            |
           |                            |
走到synchronized代码块处                 |
拿到锁之后发生了一次线程切换               |
           |                            |
           |                        走到synchronized代码块处, 拿不到锁, 被阻塞
           |                        线程切换
           |                            |
Thread 1创建了一个新实例                 |
Thread 1离开了synchronized代码块         |
锁被释放                                 |
线程切换                                 |
           |                            |
           |                            |
           |                        Thread 2拿到锁
           |                        Thread 2创建了新实例 (这里违背了单例模式原则)
           |                        Thread 2离开了synchronized代码块
           |                        Thread 2返回了创建的实例
           |                        线程切换
           |                            |
Thread 1返回创建的实例                   |
---------------------------------------------------------

这就和线程不安全的懒汉单例模式一样了

对于版本2, 其实和使用同步代码块的懒汉单例模式也是一样的, 线程是安全的, 但是性能开销依然存在

  1. 静态内部类

特点: 利用类加载机制实现懒加载, 线程安全

java 复制代码
public class Singleton {
    // 私有的构造函数, 避免从外部构造新实例
    private Singleton() {}
    
    // 静态内部类, 延迟加载
    private static class SingletonHelper {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance
    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
  1. 枚举单例

特点: 简单, 线程安全, 防止反序列化导致创建新的实例

java 复制代码
public enum Singleton {
    INSTANCE;

    // 其他方法
    public void someMethod() {
        // do something
    }
}

单例模式被破坏的情况

除了枚举单例模式之外, 其他单例模式都可以被破坏. 破坏单例模式的方法有两种, 分别为 序列化反射

  1. 序列化破坏单例模式

因为在序列化和反序列化过程中, 会创建一个新的实例, 即使单例类在内存中有一个唯一的实例, 通过反序列化也能创建多个实例, 这样就破坏了单例模式的初衷

假设有一个单例类如下:

java 复制代码
import java.io.Serializable
    
public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private static final Singleton instance = new instance();
    
    private Singleton();
    
    public Singleton getInstance() {
        return instance;
    }
    
    // other methods...
}

破坏单例模式的场景

java 复制代码
import java.io.*;

public class SingletonDemo {
    public static void main(String[] args) {
        try {
            Singleton instance1 = Singleton.getInstance();
            
            // 序列化
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
            out.writeObject(instance1);
            out.close();
            
            // 反序列化
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
            Singleton instance2 = (Singleton) in.readObject();
            in.close;
            System.out.println("Instance 1 hash code: " + instance1.hashCode());
            System.out.println("Instance 2 hash code: " + instance2.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行 SingletonDemo, 发现 instance1instance2 的哈希码并不相同, 说明它们是不同的实例, 这就破坏了单例模式

为了防止序列化破坏单例模式, 可以在单例类中定义 readResolve 方法, 这个方法在反序列化时会被调用, 返回当前的单例实例, 从而确保反序列化得到的始终是唯一的单例实例

改进之后的单例类

java 复制代码
import java.io.Serializable
    
public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private static final Singleton instance = new instance();
    
    private Singleton();
    
    public Singleton getInstance() {
        return instance;
    }
    
    // 添加readResolve方法
    protected Object readResolve() {
        return getInstance();
    }
    
    // other methods...
}

再次运行 SingletonDemo , 发现 instance1instance2 的哈希码是相同的, 因此它们是同一个实例, 单例模式没有被破坏.

  1. 反射破坏单例模式

因为反射允许我们访问私有构造方法, 从而构建多个对象, 这就违背了单例模式的初衷

假设有一个单例类如下:

java 复制代码
public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

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

破坏单例模式的场景

java 复制代码
import java.lang.reflect.Constructor;

public class SingletonDemo {
    public static void main(String[] args) {
        try {
            Singleton instance1 = Singleton.getInstance();

            // 通过反射创建新的实例
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton instance2 = constructor.newInstance();

            // 检查两个实例是否相同
            System.out.println("Instance 1 hash code: " + instance1.hashCode());
            System.out.println("Instance 2 hash code: " + instance2.hashCode());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行 SingletonDemo, 发现 instance1instance2 的哈希码并不相同. 说明它们是不同的实例, 单例模式被破坏

为了防止反射破坏单例模式, 可以在构造方法中添加防御措施, 例如在构造方法中检测实例是否存在, 如果存在就抛出异常

改进之后的单例类

java 复制代码
public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {
        // 防止反射创建新的实例
        if (instance != null) {
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

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

再次运行 SingletonDemo, 发现反射创建实例的步骤会抛出异常, 阻止了反射破坏单例模式


最近在学设计模式, 可能会高强度更新设计模式相关的技术博客. 对设计模式感兴趣的读者可以关注我的 CSDN Channel, 掘金 Channel, 我的个人博客网站网站的镜像站点

相关推荐
加油吧zkf1 分钟前
AI大模型如何重塑软件开发流程?——结合目标检测的深度实践与代码示例
开发语言·图像处理·人工智能·python·yolo
Java技术小馆3 分钟前
GitDiagram如何让你的GitHub项目可视化
java·后端·面试
ejinxian16 分钟前
PHP 超文本预处理器 发布 8.5 版本
开发语言·php
Codebee20 分钟前
“自举开发“范式:OneCode如何用低代码重构自身工具链
java·人工智能·架构
程序无bug36 分钟前
手写Spring框架
java·后端
程序无bug37 分钟前
Spring 面向切面编程AOP 详细讲解
java·前端
软件黑马王子43 分钟前
C#系统学习第八章——字符串
开发语言·学习·c#
阿蒙Amon44 分钟前
C#读写文件:多种方式详解
开发语言·数据库·c#
全干engineer1 小时前
Spring Boot 实现主表+明细表 Excel 导出(EasyPOI 实战)
java·spring boot·后端·excel·easypoi·excel导出
Da_秀1 小时前
软件工程中耦合度
开发语言·后端·架构·软件工程