0
点赞
收藏
分享

微信扫一扫

外观模式介绍

目录

一、初识

二、案例

 2.1 分析与实现-懒汉式

 2.2 分析与实现-静态内部类

 2.3 分析与实现-饿汉模式

 2.4 分析与实现-双重检查锁

 2.5 分析与实现-枚举

三、讲解

 3.1 功能

 3.2 范围

 3.3 调用顺序示意图

 3.4 优缺点

四、练习


一、初识

        

二、案例

        

 2.1 分析与实现-懒汉式

     考虑到是尝试新产品和部分地区尝试上线、硬件软件、服务器性能情况等综合因素考虑选择使用懒汉式来实例化产品Client。

     实现示例代码:

public class LazySingleton {
    private static SdkClient sdkClient;

    private LazySingleton() {
        // 私有构造函数
    }
    public static synchronized SdkClient getInstance() {
        if (sdkClient == null) {
            sdkClient = new SdkClient();
        }
        return sdkClient;
    }
}

     在懒汉模式中,构造函数是私有的,确保外部无法直接实例化该类。getInstance() 方法返回单例对象的实例,并在首次调用时创建对象。 

     优点:

  • 延迟加载,最大限度地节省资源和提高性能。
  • 只有在需要时才会创建实例,适用于大型对象或高开销资源的情况。
  • 线程安全:通过同步处理来解决多线程环境下的竞态条件。

     缺点:

  • 需要考虑线程安全问题,可能需要使用同步机制,如使用synchronized关键字或通过双重检查锁定等。
  • 同步机制会增加额外的开销,可能会影响性能。
  • 可能存在序列化和反射攻击的漏洞,需要做相应的处理来防止。

         

 2.2 分析与实现-静态内部类

     静态内部类方式利用Java的类加载机制来实现懒汉式的延迟加载。

     实现示例代码:

public class StaticInnerSingleton {
    private StaticInnerSingleton() {
        // 私有构造函数
    }

    private static class SingletonHolder {
        private static final SdkClient instance = new SdkClient();
    }

    public static SdkClient getInstance() {
        return SingletonHolder.instance;
    }
}

     在这个示例中,私有的构造函数确保了外部无法直接实例化该类的对象。通过静态内部类SingletonHolder来持有单例对象,并且该类的实例化操作是在静态初始化阶段(即类加载时)完成的,保证了线程安全性。

     优点:

  1. 懒加载:单例对象的实例化在调用getInstance()方法时才会执行,实现了延迟加载的效果。

  2. 线程安全:由于静态内部类的特性,当多个线程同时访问getInstance()方法时,静态内部类的实例化操作只会执行一次,保证了线程安全性。

  3. 简洁性:相对于双重检查锁等方式,使用静态内部类实现单例模式的代码更加简洁明了。

     缺点:

  1. 需要了解并理解静态内部类的工作原理:虽然使用静态内部类实现单例模式代码相对简洁,但是理解这种实现方式的工作原理可能需要一定的知识和经验。对于不熟悉静态内部类的开发者来说,可能需要花费一些时间来理解其背后的概念和实现细节。

  2. 不支持传递参数的实例化:静态内部类实现的单例模式无法直接传递参数给单例对象的构造函数,因为静态内部类的实例化是在类加载时完成的。如果需要传递参数的实例化,可能需要使用其他方式实现。

  3. 需要额外的类:使用静态内部类实现单例模式需要额外定义一个静态内部类来持有单例对象,这增加了代码中的类数量。虽然这对于代码的组织和结构有一定的好处,但对于一些简单的应用场景来说可能显得有些冗余。

  4. 序列化和反序列化的处理:如果单例对象需要支持序列化和反序列化操作,需要额外处理,否则在反序列化时会得到不同的实例。可以通过实现readResolve()方法来解决这个问题。

        

 2.3 分析与实现-饿汉模式

     因为支行的各种软件产品的建设都要报备到省总部,由于历史某种原因,以前报软件产品比较少。所以好多产品都集成一个应用上,一个应用几乎每有新功能需要上线(这种频繁上线方式肯定是不建议的,但有时候又是非常必须的),导致某天第一个客户经理办业务时失败,查原因是实例化和发post请求超时了(内网向外网发请求,网络上做了好多层的代理转发)。小白就想着,那就修改一下外部产品Client的实例化方式吧。

     痛点:第一个应用加载太慢导致业务办理失败。

     实现示例代码:

public class EagerSingleton {
    private static final SdkClient sdkClient = new SdkClient();
    
    private EagerSingleton() {
        // 私有构造函数
    }
    public static SdkClient getInstance() {
        return sdkClient;
    }
}

     在饿汉模式中,单例实例在类加载时就被创建,并且是静态的、final的常量。getInstance() 方法直接返回该实例,而无需进行额外的创建过程。

     由于在类加载时就创建了单例实例,因此在多线程环境中是线程安全的,不需要额外处理同步问题。

     优点:

  • 线程安全:由于在类加载时就创建实例,因此不会存在多线程并发访问创建实例的问题,无需考虑同步和线程安全。
  • 简单明了:代码相对较为简单,不需要额外的同步处理,逻辑清晰。

     缺点:

  • 资源浪费:在某些情况下,如果单例对象的创建和初始化比较耗时,而且在程序的整个生命周期中可能并不会被立即使用,就会造成资源的浪费。
  • 引入不必要的依赖:饿汉模式可能会在应用启动时加载大量实例,增加了启动时间,还可能引入不必要的依赖关系。

        

 2.4 分析与实现-双重检查锁

     有段时间发现服务器出现频繁的GC,排查发现大量的实例使用比较少但是又不能没有,其中就有外部产品Client对象。最近服务器和网络都做了调优,再加上产品已大量推广使用,在某一些高峰期可会存在大量并发的情况等等种种原因综合考虑。小白就想着,那就修改一下外部产品Client的实例化方式吧。用双重检查锁来实现实例化外部产品Client对象。

     实现示例代码:

public class DoubleCheckedSingleton {
    private volatile static SdkClient sdkClient;

    private DoubleCheckedSingleton() {
        // 私有构造函数
    }

    public static SdkClient getInstance() {
        if (sdkClient == null) {
            synchronized (SdkClient.class) {
                if (sdkClient == null) {
                    sdkClient = new SdkClient();
                }
            }
        }
        return sdkClient;
    }
}

     在双重检查锁模式中,通过在getInstance()方法中使用双重检查来确保只有在实例为null时才创建新的实例。

  • 第一次检查:检查实例是否已经创建,如果没有创建过,则进入同步块。
  • 第二次检查:在同步块中再次检查实例是否已经创建,这是为了避免在多个线程同时通过第一次检查的情况下,都进入同步块创建实例,从而造成多次实例化的问题。

     为了确保线程之间的可见性,需要使用volatile关键字修饰instance变量。volatile关键字的作用是禁止指令重排,保证每个线程都能正确看到instance变量的最新值,避免在多线程环境下出现问题。

     优点:

  • 延迟加载:双重检查锁可以在需要时才创建实例,避免了一开始就创建实例造成的资源浪费。
  • 提高性能:只有在第一次获取实例时才需要同步,后续获取实例时无需进行同步,提高了性能。

     缺点:

  • 使用双重检查锁实现单例模式仍然需要谨慎对待,确保代码正确性,代码的复杂性增加了维护和理解的难度,可能会导致潜在的bug。

        

 2.5 分析与实现-枚举

     实现示例代码:

public enum EnumSingleton {
    INSTANCE;

    private int value;

    EnumSingleton() {
        // 构造函数可以添加初始化逻辑
        value = 10;
    }

    public int getValue() {
        return value;
    }
}

     在这个示例中,EnumSingleton是一个枚举类,其中INSTANCE是唯一的枚举常量,代表了单例对象。在构造函数中,我们可以添加额外的初始化逻辑。在这里,我们将value初始化为10。

     假设在其他地方调用时,可以通过EnumSingleton.INSTANCE来获取初始对象,并调用其方法:

int value = EnumSingleton.INSTANCE.getValue();
System.out.println(value);  // 输出:10

     通过枚举类的常量INSTANCE获取到初始对象,然后可以调用其方法和访问其成员变量。 

     优点:

  • 线程安全:枚举实现的单例模式在创建实例时是线程安全的。枚举常量的实例化在类加载时完成,保证了全局只有一个实例,并且在多线程环境下也是安全的。
  • 防止反序列化创建新对象:枚举类默认实现了Serializable接口,并且枚举对象的反序列化操作不会创建新的对象。这使得枚举单例在涉及到序列化和反序列化的场景中更加安全。
  • 简单明了:使用枚举实现单例模式代码简洁明了,不需要编写复杂的线程安全逻辑或者使用双重检查锁等方式,只需声明一个枚举常量即可。

     缺点:

  • 不能延迟加载:枚举实现的单例模式在类加载时就完成了实例化,因此无法实现延迟加载的需求。如果应用在初始化时对资源消耗较大,无法延迟加载可能会影响应用的性能。
  • 有限的扩展性:枚举常量在枚举类中是固定的,无法在运行时动态地添加额外的枚举常量。这意味着枚举单例模式的扩展性相对受限,无法通过添加更多的实例来应对不同的需求。

        

三、讲解

 3.1 功能

        

 3.2 范围

        

 3.3 调用顺序示意图

28ed1e5193c9427b921aa89335f9bfcf.png

         

 3.4 优缺点

     1. 时间与空间

  • 比较懒汉式和饿汉式:懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
  • 饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断 了,节省了运行时间。

     2. 线程安全

  • 从线程安全性上讲,不加同步的懒汉式是线程不安全的。
  • 饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会 发生并发的。
  • 如何实现懒汉式的线程安全,懒汉式也是可以实现线程安全的,只要在getInstance()方法上加上synchronized 即可。加上synchronized会降低整个访问的速度,而且每次都要判断。
  • 双重检查加锁,可以使用“双重检查加锁” 的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响:并不是每次进入getlnstance 方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块这是第一重检查。进 入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

        

四、练习

     请编写 Triple类,实现最多只能生成3个 Triple类的实例,实例编号分别为 0 , 1 , 2且可以通过 getInstance(int id)来获取该编号对应的实例。

举报

相关推荐

0 条评论