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

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

日常叨逼叨

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;
    }
}

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

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(); // 启动线程
        }
    }
}

运行结果:

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;
    }
}

运行结果:

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;
    }
}

运行结果:

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;
    }
}

运行结果

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;
    }
}

运行结果

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;
    }

    // 其他需要的方法
}

运行结果:

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:本文原创,转载请注明出处


posted on 2024-02-20 08:36  ApeJ  阅读(301)  评论(0编辑  收藏  举报