设计模式——单例模式

单例模式

  • 实现单例模式的方法
    • 前置条件
    • [懒汉式(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、枚举类型的单例模式是实现单例模式的一个非常好的选择,特别是在你需要一种简单、线程安全并且能抵抗反射和序列化攻击的情况下。

相关推荐
Yang-Never9 分钟前
Shader -> RadialGradient圆心渐变着色器详解
android·java·kotlin·android studio
master-dragon42 分钟前
Spring bean的生命周期和扩展
java·spring
java1234_小锋44 分钟前
Redis有哪些常用应用场景?
java·数据库·redis
beiback1 小时前
下载导出Tomcat上的excle文档,浏览器上显示下载
java·tomcat
计算机小混子1 小时前
C++实现设计模式---访问者模式 (Visitor)
c++·设计模式·访问者模式
小学鸡!1 小时前
spring boot发送邮箱,java实现邮箱发送(邮件带附件)3中方式【保姆级教程一,代码直接用】
java·spring boot
cloud___fly1 小时前
Java常用设计模式
java·设计模式
yasinzhang2 小时前
记录IDEA与maven兼容版本
java·maven·intellij-idea
loveLifeLoveCoding2 小时前
springboot 默认的 mysql 驱动版本
java·spring boot·mysql
多多*2 小时前
后端技术选型 sa-token校验学习 下 结合项目学习 后端鉴权
java·开发语言·前端·学习·算法·bootstrap·intellij-idea