当前位置: 首页 > article >正文

设计模式之单例

单例可以说是设计模式中最简单的一种模式。但任何一种设计模式都是普遍经验的总结,都有值得思考的地方。所以单例也并不简单,下面让我们慢慢了解它。

        单例顾名思义这个类只有一个实例。要做到这点,需要做到以下几点:

        (1)构造器私有化

        (2)实例只在类内部创建,并且只会创建一次

        (3)提供静态方法以供外部获取单例对象

        单例有很多种实现方式:

        先介绍两种最简单的:懒汉式和饿汉式

(一)懒汉式:顾名思义,这个单例很懒,懒者做事就慢。所以单例对象是等要用的时候再进行实例化的。下面是一个简单的例子

public final class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (Objects.isNull(instance)) {
            instance = new Singleton();
        }
        return instance;
    }
}

(二)饿汉式:我们经常听到老一辈说别人吃饭很急,就会说“像个饿死鬼投胎一样”,所以饿汉表现就比懒汉显得急迫,在类加载时就开始实例化。

public final class Singleton {
    private static final Singleton SINGLETON = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return SINGLETON;
    }
}

        那么这两种有什么区别呢?

        我们应该注意到,单例因为在程序中只有一个实例,所以是线程共享的。那么肯定是在多线程环境下。而懒汉模式是线程不安全的,想象一下多个线程同时在单例还没实例化时进入,那么Objects.isNull(instance)判断为true,从而都会创建实例然后返回,那么有可能返回的是不同的单例。所以懒汉模式几乎不会使用,以此就衍生了线程安全的懒汉模式(这个稍后再介绍)

        而饿汉模式虽然因为类加载只会发生一次,而保证了线程安全。但可能会造成内存浪费。比如单例中可能定义了一个常量,而常量在业务场景中可能先于获取单例使用,那么在常量使用时就会进行单例实例化,如下面代码所示:

public final class Singleton {
    private static final Singleton SINGLETON = new Singleton();
    public static final String SINGLETON_CONTENT = "singleton content";

    private Singleton() {
        System.out.println("Singleton intance");
    }

    public static Singleton getSingleton() {
        return SINGLETON;
    }

    public static void main(String[] args) {
        System.out.println(Singleton.SINGLETON_CONTENT);
    }
}

(三)线程安全的懒汉式

        想要线程安全,我们首先想到的就是加synchronized锁,保证它的线程安全。于是就有了下面的实现

public final class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    public static synchronized Singleton getInstance() {
        if (Objects.isNull(instance)) {
            instance = new Singleton();
        }
        return instance;
    }
}

        但这种方式虽然线程安全了,但它的锁是方法级的。意味着每次获取实例就会加锁,性能却成了问题。于是又再次进化,有了双重校验锁的方式,可以实现高性能且安全的单例模式。

(四)双重校验锁

        上面已经说道双重校验锁是进化版的线程安全且高性能的懒汉模式。所以它也是需要用到的时候再实例化,但它是线程安全的。那么它是怎么做到的呢?我们直接上代码:

public final class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        System.out.println("Singleton intance");
    }

    public static Singleton getInstance() {
        // 第一次校验
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次校验
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

        如上代码所示,所谓双重检验锁,就是以两次校验+synchronized加锁方式实现的。第一次校验是基于性能考虑,避免每次都进行加锁,因为只有还没实例化时这个短暂且特殊的场景是需要加锁的,大部分时候都是直接返回实例就行。而第二次校验则是保证实例只会创建一次。

         另外这里有个关键点,instance单例缓存是加了volatile关键字。这是用到了volatile中的其中一个特性--禁止指令重排,想了解更多关于volatile关键字,可以参看我的另一篇《浅谈volatile》

        因为实例的创建实际会分为三个步骤

        (1)分配内存

        (2)初始化对象

        (3)将对象指向刚分配的内存空间

        如果实例是按这个顺序执行,那么不加volatile,范围的单例也是完成初始化的实力,并无影响。但因为编译器优化,可能会把因为初始化放到最后,提前先将对象指向刚分配的内存空间。那么在单例还没初始化完成时,其他线程进入获取单例时,引用对象就不是空,而提前获取到还没初始化完成的单例引用,就可能会导致后面的业务逻辑出错。

(五)静态内部类

public final class Singleton {
    private Singleton() {
        System.out.println("Singleton intance");
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

}

静态内部类也是懒加载的模式,但它的实现更优雅,是我比较推荐的方式。它利用静态内部类可以访问外部类私有构造器的特性,将单例的实例化延迟到静态内部类类加载时,保证了单例的线程安全。而这个静态内部类只会在获取单例的时候加载,所以也是懒加载。

(六)枚举方式

public enum Singleton {
    INSTANCE;

    Singleton() {
        // 初始化
    }
}

        枚举方式实现也很简单,但却最安全,因为它是反序列化。这个是前面的方式做到的。

        上面介绍了六种单例实现方式。除了第一和第三种的懒汉式不推荐使用外,其他可以根据实际需要选择。比如通常单例比较简单,那么通常饿汉模式就可以满足。需要延迟加载,那么双重校验锁、静态内部类、枚举类都可以。如果涉及到序列化,那么枚举就是唯一选择了。


http://www.kler.cn/a/419406.html

相关文章:

  • 基于 LlamaFactory 的 LoRA 微调模型支持 vllm 批量推理的实现
  • 设计模式-适配器模式-注册器模式
  • 如何使用Python进行下载对应的视频地址
  • Java之链表1
  • LabVIEW将TXT文本转换为CSV格式(多行多列)
  • 探索文件系统,Python os库是你的瑞士军刀
  • Leetcode - 周赛425
  • EditInPlace就地编辑:Dom vs Form
  • 缓存与缓冲
  • 基于PHP的音乐网站的设计与实现
  • 每日速记10道java面试题03
  • 写一份客服网络安全意识培训PPT
  • 如何分段存储Redis键值对
  • 智慧银行反欺诈大数据管控平台方案(二)
  • windows C#-为类或结构定义值相等性(上)
  • 网络原理-初识
  • 解密开源大模型如何实现本地化部署并基于本地的知识库进行应用
  • Java基础面试题11:简述System.gc()和Runtime.gc()的作用?
  • 一些面试问题的深入与思考
  • 国际网络安全趋势
  • git push使用
  • 探索Linux的目录结构:深入理解文件系统的组织
  • mongodb配置ssl连接
  • 详解Qt PDF 之 QPdfDocument与 QPdfView 打开与显示pdf
  • 如何在 Debian 7 上设置 Apache 虚拟主机
  • 时频转换 | Matlab基于S变换S-transform一维数据转二维图像方法