Spring 设计模式之单例模式

单例模式

在Java中,单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
单例模式的主要目的是限制某个类的实例化,确保系统中只有一个该类的实例在运行。

  • 唯一实例:单例模式的核心是确保一个类只有一个实例。
  • 全局访问点:提供一个全局的访问点来获取这个唯一的实例。
  • 线程安全:在多线程环境中,需要确保单例的创建过程是线程安全的,以防止创建多个实例。

一、懒汉式实现

懒汉式 是延迟加载的方式,只有使用的时候才会加载。

示例1:线程不安全、性能差的懒汉式

package com.example.customer.controller;

public class LazySingleton {
    
    

    // 声明一个静态的实例变量,初始化为null
    private static LazySingleton instance;

    // 私有构造函数,防止外部通过new创建实例
    private LazySingleton() {
    
    
        // 可以在这里进行一些初始化操作
        System.out.println("已创建 LazySingleton 实例");
    }

    // 提供一个公共的静态方法,用于获取实例
    public static LazySingleton getInstance() {
    
    
        if (instance == null) {
    
    
            instance = new LazySingleton();
        }
        return instance;
    }

    // 提供一个简单的示例方法
    public void showMessage(){
    
    
        System.out.println("来自 LazySingleton 实例的你好!");
    }

    public static void main(String[] args) {
    
    
        // 测试单例模式
        Runnable task = () -> {
    
    
            LazySingleton singleton = LazySingleton.getInstance();
            singleton.showMessage();
        };

        // 创建多个线程来访问getInstance()方法
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

输出结果:
会出现创建了多个实例,显然违反了单例模式的定义

已创建 LazySingleton 实例
已创建 LazySingleton 实例
来自 LazySingleton 实例的你好!
已创建 LazySingleton 实例
来自 LazySingleton 实例的你好!
来自 LazySingleton 实例的你好!

出现问题:

  • 线程安全问题:
    在没有同步的情况下,如果多个线程同时调用 getInstance() 方法,并且 instance 变量为 null,则可能会有多个 LazySingleton 实例被创建。这显然违反了单例模式的定义。
  • 性能问题:
    即使在同步的情况下,每次调用 getInstance() 方法时都需要进行同步,这会导致性能下降。

示例2:线程安全、性能差的懒汉式

通过synchronized ,解决了线程不安全的情况

package com.example.customer.controller;

public class LazySingleton {
    
    

    // 声明一个静态的实例变量,初始化为null
    private static LazySingleton instance;

    // 私有构造函数,防止外部通过new创建实例
    private LazySingleton() {
    
    
        // 可以在这里进行一些初始化操作
        System.out.println("已创建 LazySingleton 实例");
    }

    // 提供一个公共的静态方法,用于获取实例
    // 加上了synchronized
    public static synchronized LazySingleton getInstance() {
    
    
        if (instance == null) {
    
    
            instance = new LazySingleton();
        }
        return instance;
    }

    // 提供一个简单的示例方法
    public void showMessage(){
    
    
        System.out.println("来自 LazySingleton 实例的你好!");
    }

    public static void main(String[] args) {
    
    
        // 测试单例模式
        Runnable task = () -> {
    
    
            LazySingleton singleton = LazySingleton.getInstance();
            singleton.showMessage();
        };

        // 创建多个线程来访问getInstance()方法
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

输出结果:
使用同步块后,输出应该只显示一个实例被创建,但每次调用 getInstance() 都会进行同步,性能较低。

已创建 LazySingleton 实例
来自 LazySingleton 实例的你好!
来自 LazySingleton 实例的你好!
来自 LazySingleton 实例的你好!

示例3:线程安全、性能好的懒汉式(也有用)

双重检查锁(Double-Checked Locking):
优点:实现了懒加载,且在多线程环境下保证了线程安全,同时提高了性能。
缺点:实现相对复杂,需要正确使用volatile关键字来防止指令重排序。
适用场景:适用于需要高性能且需要懒加载的单例模式,特别是在多线程并发访问较为频繁的场景。

package com.example.customer.controller;

public class LazySingleton {
    
    

    // 使用volatile关键字确保多线程正确处理instance变量
    private static volatile LazySingleton instance;

    // 私有构造函数,防止外部通过new创建实例
    private LazySingleton() {
    
    
        // 可以在这里进行一些初始化操作
        System.out.println("已创建 LazySingleton 实例");
    }

    // 提供一个公共的静态方法,用于获取实例
    public static LazySingleton getInstance() {
    
    
        if (instance == null) {
    
     // 第一次检查,无需同步
            synchronized (LazySingleton.class) {
    
    
                if (instance == null) {
    
     // 第二次检查,需要同步
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

    // 提供一个简单的示例方法
    public void showMessage(){
    
    
        System.out.println("来自 LazySingleton 实例的你好!");
    }

    public static void main(String[] args) {
    
    
        // 测试单例模式
        Runnable task = () -> {
    
    
            LazySingleton singleton = LazySingleton.getInstance();
            singleton.showMessage();
        };

        // 创建多个线程来访问getInstance()方法
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

在这个示例中使用了 volatile 和 双重检查锁定优化技术。

instance 变量被声明为 volatile。这是非常重要的,因为 volatile 关键字有几个关键特性,它确保:

扫描二维码关注公众号,回复: 17418876 查看本文章
  1. 变量的修改会对其他线程立即可见(这个才是重点)。
  2. 禁止指令重排序优化,这可以确保在创建对象时,instance 的赋值操作发生在构造函数调用之后(即使在没有同步的情况下)。

双重检查锁定的步骤如下:

  1. 在没有同步的情况下检查 instance 是否为 null。如果 instance 不为 null,则直接返回它。
  2. 如果 instancenull,则进入同步块。
  3. 在同步块内,再次检查 instance 是否为 null(这是必要的,因为可能有另一个线程已经通过了第一个检查并正在创建实例)。
  4. 如果 instance 仍然为 null,则创建实例。

这种方法结合了延迟初始化和线程安全的优点,同时减少了同步带来的性能开销。

二、饿汉式实现 (常用方式)

实现方式:在类加载时就创建实例。
优点:线程安全,因为实例在类加载时就已经被创建。
缺点:如果实例在程序运行过程中没有被使用到,会占用内存空间。
适用场景:适用于单例对象在类加载时就需要被初始化,且不会造成内存浪费的情况。

示例代码:

package com.example.customer.controller;

public class EagerSingleton {
    
    
    // 在类加载时就创建好的单例实例,由于是final修饰的,所以一旦被赋值后就不能再改变
    private static final EagerSingleton instance = new EagerSingleton();

    // 私有的构造方法,防止外部通过new关键字来创建实例
    private EagerSingleton() {
    
    
        // 在创建实例时打印一条消息到控制台
        System.out.println("已创建 EagerSingleton 实例");
    }

    // 提供一个公共的静态方法来获取单例实例
    public static EagerSingleton getInstance() {
    
    
        return instance;
    }

    // 提供一个简单的示例方法
    public void showMessage() {
    
    
        System.out.println("来自 EagerSingleton 实例的你好!");
    }

    public static void main(String[] args) {
    
    
        // 测试单例模式
        Runnable task = () -> {
    
    
            EagerSingleton singleton = EagerSingleton.getInstance();
            singleton.showMessage();
        };

        // 创建多个线程来访问getInstance()方法
        // 注意:由于EagerSingleton的实例在类加载时已经创建,
        // 因此无论创建多少个线程,控制台上关于实例创建的打印信息只会出现一次。
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

输出结果:
实例是在类加载时创建的,因此“已创建 EagerSingleton 实例”这条消息只会出现一次。

已创建 EagerSingleton 实例
来自 EagerSingleton 实例的你好!
来自 EagerSingleton 实例的你好!
来自 EagerSingleton 实例的你好!

三、静态内部类实现(常用方式)

实现方式:在静态内部类中创建实例,外部类加载时并不会立即加载内部类,从而实现了延迟加载和线程安全。
优点:既实现了延迟加载,又保证了线程安全,且无需额外的同步开销。
适用场景:适用于需要懒加载的单例对象,且希望保持线程安全的场景。

示例代码:

package com.example.customer.controller;

public class StaticInnerClassSingleton {
    
    

    // 私有构造函数,防止外部通过new关键字创建实例
    private StaticInnerClassSingleton() {
    
    
        System.out.println("已创建 StaticInnerClassSingleton 实例");
    }

    // 静态内部类,负责持有单例实例
    // 静态内部类只有在被引用时才会被加载,从而实现了延迟加载的效果
    private static class SingletonHelper {
    
    
        // 使用final关键字确保单例实例的唯一性和不可变性
        // 当SingletonHelper类被加载时,INSTANCE会被初始化
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    // 公共的静态方法,用于获取单例实例
    // 由于SingletonHelper是静态内部类,它会在第一次被引用时加载,从而实例化INSTANCE
    public static StaticInnerClassSingleton getInstance() {
    
    
        return SingletonHelper.INSTANCE;
    }

    // 示例方法,用于展示单例实例的功能
    public void showMessage() {
    
    
        System.out.println("来自StaticInnerClassSingleton实例的你好!");
    }

    // 测试静态内部类单例模式的main方法
    public static void main(String[] args) {
    
    
        // 创建多个线程来访问getInstance()方法
        Runnable task = () -> {
    
    
            StaticInnerClassSingleton singleton = StaticInnerClassSingleton.getInstance();
            singleton.showMessage();
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();

    }
}

输出结果:
实例是在类加载时创建的,因此“已创建 EagerSingleton 实例”这条消息只会出现一次。

已创建 StaticInnerClassSingleton 实例
来自StaticInnerClassSingleton实例的你好!
来自StaticInnerClassSingleton实例的你好!
来自StaticInnerClassSingleton实例的你好!

四、枚举(Enum)实现

实现方式:通过枚举类型来实现单例模式。
优点:简单、线程安全,且能够防止反序列化重新创建新的对象。
缺点:不是懒加载的。
适用场景:适用于需要防止反序列化创建新对象的单例模式,以及希望代码简洁、易读、易维护的场景。

  • 示例代码
public enum EnumSingleton {
    
    
    INSTANCE;

    public void doSomething() {
    
    
        // 示例方法
    }
}

五、使用容器实现单例模式

  • 实现方式:通过某种容器(如Map)来统一管理多种单例类,使用时根据key获取对应的单例对象。
  • 优点:可以管理多种类型的单例,降低用户的使用成本和耦合度。
  • 示例代码(简化版):
import java.util.HashMap;
import java.util.Map;

public class SingletonManager {
    
    
    private static Map<String, Object> objMap = new HashMap<>();

    public static void registerService(String key, Object instance) {
    
    
        if (!objMap.containsKey(key)) {
    
    
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
    
    
        return objMap.get(key);
    }
}

// 使用示例
public class SomeSingleton {
    
    
    private SomeSingleton() {
    
    }

    public static void init() {
    
    
        SingletonManager.registerService("SomeSingleton", new SomeSingleton());
    }

    public void doSomething() {
    
    
        // 示例方法
    }
}

(注意:这种方式并不是一种典型的单例模式实现,但它提供了一种灵活的方式来管理多种单例对象。)

综上所述,实现单例模式的方式有多种,选择哪种方式取决于具体的应用场景和需求。在实际开发中,推荐使用静态内部类或枚举方式来实现单例模式,因为它们既简单又高效,且能够很好地保证线程安全。

扩展—Java类中元素的加载顺序

在Java中,类里面的元素加载顺序遵循一定的规则,这些规则确保了类的初始化和实例化的过程是有序且可预测的。以下是Java类中元素的加载顺序:

  1. 静态变量和静态代码块

    • 当类被加载时(即类第一次被引用时,且只加载一次),静态变量和静态代码块会按照它们在类中出现的顺序被初始化。
    • 静态变量包括静态成员变量和静态常量(用final修饰的静态变量在编译期就已经确定值,但它们的初始化仍然是在类加载时进行的,只不过它们的值在编译期就已经被确定了)。
    • 静态代码块是包含在类中的静态初始化块,它们会在类加载时被执行。
  2. 实例变量和实例初始化块

    • 当创建类的实例时,实例变量和实例初始化块会按照它们在类中出现的顺序被初始化。
    • 实例变量包括成员变量(非静态变量)。
    • 实例初始化块是包含在类中的非静态初始化块,它们会在每次创建实例时被执行。
  3. 构造函数

    • 在实例变量和实例初始化块初始化之后,会调用类的构造函数来完成对象的创建。
    • 构造函数可以是无参的,也可以是有参的,它们定义了对象在创建时需要进行的初始化操作。
  4. 父类和子类的加载顺序

    • 当加载一个子类时,会首先加载其父类(如果父类还没有被加载的话),并按照上述规则初始化父类的静态变量、静态代码块、实例变量、实例初始化块和构造函数。
    • 然后,才会按照同样的规则初始化子类的静态变量、静态代码块、实例变量、实例初始化块和构造函数。
  5. 注意

    • 静态变量和静态代码块在类加载时只初始化一次,而实例变量和实例初始化块在每次创建实例时都会初始化。
    • 如果类中有多个静态代码块或多个实例初始化块,它们会按照在类中出现的顺序被依次执行。
    • 构造函数可以被重载(即可以有多个构造函数,但它们的参数列表必须不同),但每次创建实例时只会调用一个构造函数(根据创建实例时提供的参数来确定)。
  6. volatile关键字的作用

    • 在上述加载顺序中,volatile关键字主要用于修饰静态变量或实例变量,以确保变量的可见性和禁止指令重排序。
    • 当一个变量被volatile修饰时,线程在读取这个变量的值时,将直接从主内存中读取,而不会使用线程自己的本地缓存。这样可以确保多个线程在读取这个变量时都能看到最新的值,从而避免了线程之间的数据不一致性问题。
    • 此外,volatile还可以禁止编译器和处理器对修饰的变量进行指令重排序优化,从而确保线程能够按照程序的顺序执行。

综上所述,Java类中元素的加载顺序是一个有序且可预测的过程,它确保了类的初始化和实例化的正确性。同时,volatile关键字在这个过程中起到了确保变量可见性和禁止指令重排序的重要作用。

猜你喜欢

转载自blog.csdn.net/qq_20236937/article/details/143193445