• 设计模式浅析(五) ·单例模式


    设计模式浅析(五) ·单例模式

    日常叨逼叨

    java设计模式浅析,如果觉得对你有帮助,记得一键三连,谢谢各位观众老爷😁😁


    单例模式

    概念

    单例模式确保一个类只有一个实例,并提供一个全局访问点。

    懒汉式:线程不安全

    那么怎么构建一个单例模式,使得只返回唯一一个对象实例呢,我这里提供了一种方法

    public class SingleInstance {
        //利用一个静态变量来记录SingleInstance类的唯一实例。
        public static SingleInstance singleInstance;
    
        //把构造器声明为私有的,只有SingleInstance类内才可以调用构造器
        private SingleInstance() {
        }
    
        public static SingleInstance getInstance() {
    
            //如果它不存在,我们就利用私有的构造器产生一个SingleInstance实例并把它赋值到singleInstance静态变量中。请注意,如果我们不需要这个实例,它就永远不会产生。这就是“延迟实例化”(laxy instantiaze)
            if (singleInstance == null) {
                singleInstance = new SingleInstance();
            }
            //如果singleInstance不是null,就表示之前已经创建过对象。我们就直接跳到return语句。
            return singleInstance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    看起来好像是没有什么大的问题 思路清晰,代码明确。那么我们写如下代码进行测试

    public class Client extends Thread {
        @Override
        public void run() {
            // 线程执行的代码
            SingleInstance instance = SingleInstance.getInstance();
    
            System.out.println(instance + " <线程"+Thread.currentThread().getId()+"正在运行>");
        }
        public static void main(String[] args) {
            int i = 0;
            while (i < 5) {
                i++;
                Client myThread = new Client();
                myThread.start(); // 启动线程
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    运行结果:

    com.jerry.singlePattern.SingleInstance@294eb59e <线程24正在运行>
    com.jerry.singlePattern.SingleInstance@4c3154fa <线程23正在运行>
    com.jerry.singlePattern.SingleInstance@294eb59e <线程20正在运行>
    com.jerry.singlePattern.SingleInstance@294eb59e <线程21正在运行>
    com.jerry.singlePattern.SingleInstance@4add1757 <线程22正在运行>

    运行结果却有点出乎意料???我们不是单例模式吗?怎么创建出这么多的SingleInstance对象??

    出现上述问题的原因是多线程

    在这个实现方式中,getInstance() 方法会检查 singleInstance 是否为 null,如果是则创建一个新的 singleInstance 实例。问题出在多个线程可能同时检查到 singleInstancenull,然后每个线程都创建一个新的实例,这就违背了单例模式的初衷。

    具体来说,假设有两个线程 T1 和 T2 同时调用 getInstance() 方法。由于 singleInstance 的初始值为 null,T1 和 T2 都会进入 if (singleInstance== null) 的判断。由于这两个线程是并发执行的,它们可能会同时进入这个判断条件,并且都通过判断,然后各自创建一个新的 singleInstance 实例。这就导致了多个实例被创建,违反了单例模式的规则。

    懒汉式:线程安全(同步方法)

    我们可以将上述代码稍加修改,通过增加synchronized关键手到getInstance()方法中,我们迫使每个线程在进入这个方法之前,要先等候别的线程离开该方法。也就是说,不会有两个线程可以同时进入这个方法。

    public class SingleInstance2 {
        public static SingleInstance2 singleInstance;
    
        private SingleInstance2() {
        }
    
        public static synchronized SingleInstance2 getInstance() {
    
            if (singleInstance == null) {
                singleInstance = new SingleInstance2();
            }
            return singleInstance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:

    com.jerry.singlePattern.SingleInstance2@5a506132 <线程23正在运行>
    com.jerry.singlePattern.SingleInstance2@5a506132 <线程21正在运行>
    com.jerry.singlePattern.SingleInstance2@5a506132 <线程22正在运行>
    com.jerry.singlePattern.SingleInstance2@5a506132 <线程24正在运行>
    com.jerry.singlePattern.SingleInstance2@5a506132 <线程20正在运行>

    虽然实现了同步,实现了单例,但是上述方法的确是有一点不好。而比想象的还要严重一些的是:只有第一次执行此方法时,才真正需要同步。换句话说,一旦设置好singleInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。

    懒汉式:线程安全(双重检查锁定)

    那么在进行一些程序上的修改,利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,“才”进行同步。这样一来,只有第一次会同步,这正是我们想要的。

    public class SingleInstance3 {
        public static volatile SingleInstance3 singleInstance;
    
        private SingleInstance3() {
        }
    
        public static SingleInstance3 getInstance() {
            if (singleInstance == null) {
                synchronized (SingleInstance3.class) {
                    if (singleInstance == null) {
                        singleInstance = new SingleInstance3();
                    }
                }
            }
            return singleInstance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    运行结果:

    com.jerry.singlePattern.SingleInstance3@1aef3070 <线程24正在运行>
    com.jerry.singlePattern.SingleInstance3@1aef3070 <线程21正在运行>
    com.jerry.singlePattern.SingleInstance3@1aef3070 <线程20正在运行>
    com.jerry.singlePattern.SingleInstance3@1aef3070 <线程23正在运行>
    com.jerry.singlePattern.SingleInstance3@1aef3070 <线程22正在运行>

    这种实现方式既保证了线程安全,又避免了不必要的同步,提高了效率。但是需要注意的是,Java 1.5 以前的版本对 volatile 的支持并不完善,因此在 Java 1.5 以前的版本中使用双重检查锁定可能会存在问题。

    除了上述的几种方案,还有一些方案实现单例模式:

    饿汉式:线程安全
    public class SingleInstance1 {
        // 在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
        public static SingleInstance1 singleInstance=new SingleInstance1();
    
       private SingleInstance1() {
        }
    
        public static SingleInstance1 getInstance() {
            return singleInstance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行结果

    com.jerry.singlePattern.SingleInstance1@7070e0ed <线程20正在运行>
    com.jerry.singlePattern.SingleInstance1@7070e0ed <线程24正在运行>
    com.jerry.singlePattern.SingleInstance1@7070e0ed <线程21正在运行>
    com.jerry.singlePattern.SingleInstance1@7070e0ed <线程23正在运行>
    com.jerry.singlePattern.SingleInstance1@7070e0ed <线程22正在运行>

    在这个例子中,SingleInstance1类的singleInstance成员变量在类加载时就被初始化了,由于JVM的类加载机制是线程安全的,所以这个过程是线程安全的。因此,后续的getInstance()方法调用都是直接返回这个已经初始化好的instance,不需要额外的同步措施。

    这就是饿汉式单例模式如何保证线程安全的方式。由于它在类加载时就完成了初始化,所以不存在多线程并发访问的问题,因此是线程安全的。

    静态内部类:线程安全
    public class SingleInstance4 {
        private static class SingletonHolder {
            private static final SingleInstance4 INSTANCE = new SingleInstance4();
        }
        private SingleInstance4 (){}
    
        public static final SingleInstance4 getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行结果

    com.jerry.singlePattern.SingleInstance4@7004943 <线程23正在运行>
    com.jerry.singlePattern.SingleInstance4@7004943 <线程20正在运行>
    com.jerry.singlePattern.SingleInstance4@7004943 <线程24正在运行>
    com.jerry.singlePattern.SingleInstance4@7004943 <线程21正在运行>
    com.jerry.singlePattern.SingleInstance4@7004943 <线程22正在运行>

    静态内部类SingletonHolder中包含了一个静态字段INSTANCE,这个字段在静态内部类被加载时初始化。由于JVM的类加载机制保证了类的初始化过程是线程安全的,因此静态内部类中的静态字段只会被初始化一次,即在首次通过getInstance()方法访问时。这就确保了单例的懒加载和线程安全。

    Singleton类被加载时,SingletonHolder类并不会立即被加载。只有当调用Singleton.getInstance()方法时,SingletonHolder类才会被加载,此时会初始化INSTANCE字段。由于类的加载和初始化是由JVM在内部通过锁机制保证线程安全的,所以不需要额外的同步措施。

    因此,静态内部类实现单例模式既能够实现懒加载,又能够利用JVM的类加载机制保证线程安全。这是单例模式中一种非常优雅且高效的实现方式。

    枚举:线程安全
    public enum SingleInstance5 implements Serializable {
        INSTANCE_5;
    
        // 如果需要,可以添加其他属性或方法
        private String data;
    
        public void setData(String data) {
            this.data = data;
        }
    
        public String getData() {
            return data;
        }
    
        // 其他需要的方法
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果:

    INSTANCE_5 <线程20正在运行>
    INSTANCE_5 <线程23正在运行>
    INSTANCE_5 <线程22正在运行>
    INSTANCE_5 <线程24正在运行>
    INSTANCE_5 <线程21正在运行>

    在这个例子中,SingleInstance5 是一个枚举类型,它只有一个实例 INSTANCE。由于枚举的特性,这个实例是线程安全的,并且在整个应用程序中都是唯一的。

    要获取这个单例的实例,只需要调用 SingleInstance5.INSTANCE。要访问或修改它的属性,可以使用 SingleInstance5.INSTANCE.setData(data)SingleInstance5.INSTANCE.getData() 方法。

    关于序列化,由于枚举实例在反序列化时会被当作单个对象处理,因此不会出现重新创建实例的情况。也就是说,即使你尝试序列化并反序列化 SingletonEnum.INSTANCE,你得到的仍然是同一个实例。

    使用枚举实现单例模式的好处是简单、高效且线程安全,不需要担心多线程环境下的竞态条件或其他同步问题。此外,由于枚举类型在Java中是特殊的类,它们不能被继承或反射实例化,这进一步增强了单例的安全性。

    优缺点
    优点:
    1. 节省资源:由于系统内存中只存在一个实例,因此可以节约系统资源,尤其是对于那些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
    2. 简化代码:单例模式提供了一个全局访问点,可以方便地在应用程序的任何地方访问该实例,无需频繁地创建和销毁对象,从而简化了代码。
    3. 避免数据不一致:单例模式可以确保所有对象都访问同一个实例,从而避免了由于多个实例导致的数据不一致问题。
    缺点:
    1. 扩展性差:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。如果需要扩展单例类的功能,通常需要修改源代码,这违反了开闭原则。
    2. 职责过重:单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起,导致职责过重,在一定程度上违背了“单一职责原则”。
    3. 线程安全问题:在多线程环境下,单例模式的实现需要考虑线程安全问题。如果实现不当,可能会导致多个实例被创建,从而引发数据不一致的问题。
    4. 滥用可能导致问题:如果滥用单例模式,例如将数据库连接池设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

    代码相关代码可以参考 代码仓库🌐

    ps:本文原创,转载请注明出处


  • 相关阅读:
    了解JS中的混个对象“类”
    一个例子形象的理解协程和线程的区别
    动态数组底层是如何实现的
    记一次【RabbitMQ集群网络分区】的问题,以及网络分区时的影响范围和如何恢复
    Spark SQL_第六章笔记
    web组态(BY组态)接入流程
    mysql 5.7 容器启动命令
    一文读懂 Spring Bean 的生命周期
    kubernetes 起几个节点,就会有几个flannel pod
    web 前端面试题
  • 原文地址:https://blog.csdn.net/Jerrylau213/article/details/136174735