设计模式——单例模式

单例模式

  • 实现单例模式的方法
    • 前置条件
    • [懒汉式(Lazy Initialization)](#懒汉式(Lazy Initialization))
    • [饿汉式(Eager Initialization)](#饿汉式(Eager Initialization))
    • [双重锁式(Double-Checked Locking)](#双重锁式(Double-Checked Locking))
    • [静态内部类式(Static Inner Class)](#静态内部类式(Static Inner Class))
    • 枚举式(Enum)
  • 总结

单例模式 (Singleton Pattern)是软件工程中的一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

实现单例模式的方法

前置条件

创建一个User类,模拟单例模式中创建对象使用。

c 复制代码
public class User {
    private Integer id;
    private String name;
    private String password;
    public User() {
    }

    public User(Integer id, String name, String password) {
        this.id = id;
        this.name = name;
        this.password = password;
    }
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

懒汉式(Lazy Initialization)

单例模式中常见的模式之一,懒汉式可以做到使用单例对象时才创建对象,可以实现延迟加载,但是存在线程安全问题,需要通过synchronized关键字保证了线程安全,但会影响性能。

c 复制代码
/*懒汉式 线程不安全 需要使用 synchronized*/
public class Lazy {

    private static User user;

    //没有synchronized关键字,线程不安全,多线程调用此方法时会创建不同地址值的User对象
    //对外提供接口
    public static synchronized User getUser() {
        if (user == null) {
            user = new User(1,"zhao","123456");
        }
        return user;
    }


    public static void main(String[] args) {
        //测试多线程下的懒汉式
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                User user = getUser();
                System.out.println(user);
            }
            ).start();
        }
    }
}

若没有添加synchronized关键字执行结果为:

会出现创建出多个对象的情况,背离了单例模式的初初衷。

添加synchronized关键字后可以保障对象的唯一性

饿汉式(Eager Initialization)

单例模式中常见的模式之一,饿汉式是在类加载时就创建实例比较简单,可以保证线程安全,但不支持延迟加载。

c 复制代码
/*
* 饿汉式 线程安全
* */
public class Hungry {
    //类加载时就创建实例
    private static final User user = new User(1,"zhao","123456");
    //对外提供接口
    public static User getUser(){
        return user;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                User user = getUser();
                user.setId(2);
                System.out.println(user);
            }
            ).start();
        }
    }
}

运行结果:

实现了单例模式

双重锁式(Double-Checked Locking)

双锁结构,优化懒汉式,为了提高性能同时保持线程安全性,可以采用双重检查锁定的方式。

volatile关键字: 是一种轻量级的同步机制,适用于那些不需要复杂同步逻辑的简单场景。如果应用需要更复杂的并发控制,那么应该考虑使用更高级的同步工具

优点:

1、可见性:

当一个线程修改了 volatile 变量的值,这个修改会立即对其他线程可见。

每次读取 volatile 变量时,都会直接从主内存中读取最新的值,而不是使用缓存中的旧值。

每次写入 volatile 变量时,都会立即将更新后的值写回主内存,确保其他线程可以看到最新的状态。

2、禁止指令重排序:

Java 编译器和处理器为了优化性能,可能会对指令进行重排序。然而,对于 volatile 变量的操作,编译器和运行时环境都必须遵守一定的规则,不能将 volatile 写操作放到读操作之后,也不能将 volatile 读操作放到写操作之前。这有助于维持程序的逻辑正确性。

缺点:

1、不保证原子性:

尽管 volatile 提供了可见性和有序性,但它并不提供原子性。这意味着如果对 volatile 变量执行复合操作(如 i++),这些操作仍然可能受到竞态条件的影响,因为它们不是原子性的。要确保原子性,可以考虑使用同步机制、Atomic 类或锁等方法。

synchronized 关键字: 是 Java 中用于实现线程同步的关键字,它能够确保在多线程环境中对共享资源的安全访问。

c 复制代码
/*
双锁结构,优化懒汉式,为了提高性能同时保持线程安全性,可以采用双重检查锁定的方式。
 */
public class DoubleChecked {
    // 使用volatile避免指令重排序,保证user的可见性
    private static volatile User user;

    //对外提供接口
    public static User getUser() {
        if (user == null) {
            synchronized (User.class) {
                if (user == null) {
                    user = new User(1, "zhao", "123456");
                }
            }
        }
        return user;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                User user = getUser();
                System.out.println(user);
            }
            ).start();
        }
    }
}

运行结果:

静态内部类式(Static Inner Class)

利用了Java语言的特性,只有当内部类被加载时才会创建单例实例,因此它是线程安全且延迟加载的。
静态内部类: 只有在它被显式使用时才会被加载,比如创建静态内部类的实例或者访问其静态成员。

c 复制代码
/*
* 内部类 利用了Java语言的特性,只有当内部类被加载时才会创建单例实例,因此它是线程安全且延迟加载的。
*/
public class StaticInnerClass {

    //创建内部类:只有当内部类被加载时才会创建单例实例
    private static class GetUserInnerClass{
        private static final User user = new User(1,"zhao","123456");
    }

    //对外提供接口
    public static User getUser(){
        return GetUserInnerClass.user;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                User user = getUser();
                System.out.println(user);
            }
            ).start();
        }
    }
}

运行结果:

枚举式(Enum)

枚举类型的单例模式之所以线程安全,主要是因为Java语言规范对枚举类型的初始化提供了保证,并且在多个方面限制了创建新实例的可能性。以下是具体的原因:
1. 类加载机制

Java的类加载器在加载类时是线程安全的。当一个类第一次被加载到JVM中时,类加载器会确保这个过程是原子性的,即同一时间只有一个线程可以执行类的加载和初始化。对于枚举类型来说,它的静态字段(包括枚举常量)是在类加载期间初始化的,这意味着所有枚举实例的创建都是在这个安全的过程中完成的。

2. 构造函数私有化

枚举类型的构造函数默认是私有的,这防止了外部代码通过new关键字来创建新的枚举实例。即使使用反射技术尝试调用私有构造函数,在Java 5及以上版本中,JVM也特别处理了这种情况,以确保无法通过反射创建额外的枚举实例。

3. 静态初始化块

枚举实例是通过静态初始化块创建的,而静态初始化块只会在类加载时执行一次,并且由JVM保证其线程安全性。由于枚举实例是静态成员变量的一部分,它们的初始化也是线程安全的。

4. 序列化机制

Java的序列化机制为枚举类型提供了特殊的支持。当试图反序列化一个枚举实例时,JVM不会创建一个新的对象,而是返回已经存在的枚举常量。如果有人尝试通过反序列化来创建新的枚举实例,JVM会抛出InvalidObjectException异常,从而防止了这种攻击。

5. 克隆保护

枚举类型还重写了Cloneable接口中的clone()方法,使其抛出CloneNotSupportedException异常。这阻止了通过克隆方式创建新的枚举实例。

6. 反射保护

如前所述,尽管反射可以用来访问私有构造函数或字段,但Java的反射API对枚举类型进行了特殊的处理,使得无法利用反射来创建新的枚举实例。

c 复制代码
//枚举实现单例模式
//枚举类型本质上就是线程安全的,并且默认情况下是不可变和序列化的,非常适合用来实现单例模式。
public enum Enum {
    GETUSER;

    //枚举实现单例模式
    private User user;
    //构造方法私有化,创建user对象
    Enum() {
        user = new User(1,"zhao","123456");
    }
    //获取user对象
    public User getUser() {
        return user;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Enum e = Enum.GETUSER;
                User user = e.getUser();
                System.out.println(user);
            }
            ).start();
        }
    }
}

运行结果

总结

1. 饿汉式(Eager Initialization)

特点:类加载时即创建实例。

优点:线程安全,实现简单。

缺点:不支持延迟加载,可能浪费资源。
2. 懒汉式(Lazy Initialization)

特点:第一次调用 getInstance() 方法时才创建实例。

优点:支持延迟加载。

缺点:基本实现不是线程安全的;需要额外措施确保线程安全(如同步整个方法或使用双重检查锁定)。
3. 双重检查锁定(Double-Checked Locking)

特点:懒加载且线程安全,只在第一次创建实例时加锁。

优点:线程安全,支持延迟加载,性能较好。

缺点:实现稍微复杂一些。
4. 静态内部类(Static Inner Class)

特点:利用了Java语言特性,只有当静态内部类被加载时才会创建单例实例。

优点:线程安全,支持延迟加载,代码简洁。

缺点:相对不太直观。
5. 枚举(Enum)

特点:最简洁、最安全的方式,天然防止反射和序列化攻击。

优点:线程安全,支持延迟加载,代码非常简洁,防止反序列化攻击。

缺点:扩展性有限,不能继承其他类(只能实现接口)。

选择哪种方式?

1、如果你确定你的应用不会有多线程问题或者你不关心性能,那么饿汉式可能是最简单的解决方案。

2、如果你需要延迟加载并且希望保持线程安全,那么建议使用双重检查锁定、静态内部类或枚举的方式来实现懒汉式单例模式。

3、枚举类型的单例模式是实现单例模式的一个非常好的选择,特别是在你需要一种简单、线程安全并且能抵抗反射和序列化攻击的情况下。

相关推荐
Zz_waiting.11 分钟前
多线程进阶
java·开发语言·jvm·javaee
你们补药再卷啦18 分钟前
集成nacos2.2.1出现的错误汇总
java·后端·spring
cosmos31524 分钟前
Java多线程断点下载 - 随机存取
java
吾日三省吾码25 分钟前
改善 Maven 的依赖性
java·maven
快乐源泉28 分钟前
【设计模式】啊?没听过命令模式,有什么优点?
后端·设计模式
来自星星的坤34 分钟前
如何在 Postman(测试工具) 中实现 Cookie 持久化并保持同一会话
java·开发语言·spring boot·后端
爱的叹息37 分钟前
Spring MVC 重定向(Redirect)详解
java·spring·mvc
xrkhy1 小时前
面向对象高级(1)
java·开发语言
五行星辰1 小时前
Spring定时任务修仙指南:从@Scheduled到分布式调度的终极奥义
java·后端·spring
昂子的博客1 小时前
热门面试题第15天|最大二叉树 合并二叉树 验证二叉搜索树 二叉搜索树中的搜索
java·数据结构·算法