前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >灵活多变的单例模式

灵活多变的单例模式

作者头像
啃饼思录
发布2021-11-02 14:57:35
2840
发布2021-11-02 14:57:35
举报

写在前面

不用再催更了,我开始更新了,点击卡片快速关注我~~~

在软件工程领域,设计模式是一套通用、可复用的解决方案,用于解决在软件设计过程中产生的通用问题。它不是一个可以直接转成源码的设计,是一套开发人员在软件设计过程中应当遵循的规范。也就是说没有设计模式,软件依旧可以开发,只是后期维护可能变得不那么轻松。设计模式就是为了简化你的维护成本提升性能而设计的,不同的设计模式适用场景各异,具体的结合实际场景对待。

单例模式

定义

本篇来学习关注对象创建类型中的单例模式(Singleton Pattern),单例模式是指一个类有且只有一个实例,并且自行实例化向整个系统提供该实例,它的目的就是使得类的一个对象成为该系统中的唯一实例。

使用场景

接下来介绍单例模式的使用场景,设计模式只是一种规范,因此了解在何种场景下选择哪种设计模式可能比具体实现显得更为重要。当你希望整个系统在运行时某个类有且只有一个实例的时候,建议使用单例模式来创建该类对象。

核心点

单例模式有两个核心点:(1)如何保证单例?在多线程环境下,整个系统在运行时如何做到某个类有且只有一个实例?如何保证不通过反射来创建新的实例?(2)如何创建单例?总的来说,创建单例的方式分为两大类:饿汉式和懒汉式。所谓饿汉式,是指无论你用不用这个实例,我先给你创建出来;而懒汉式与之相反,只有你需要的时候,我才去创建这个实例。由此可以得出懒汉式和饿汉式最大的区别就是创建对象的时机不同。

要点

单例模式有三个要点:(1)某个类只能有一个实例;(2)必须自行创建实例;(3)必须自行向整个系统提供这个实例。

实现

单例模式的实现有三个步骤,分别对应前面所述的三个要点:(1)只提供私有的构造方法;(2)只含有一个该类的静态私有对象;(3)提供一个静态的公有方法用于创建、获取静态私有对象。

接下来将对这三个步骤进行解释。对于(1)的理解,private是访问限制能力最强的修饰符,只能在当前类中使用。也就说经过private的修饰,该类的对象在类外无法通过new关键字直接实例化,这样可以做到限制类实例化产生。

对于(2)的理解,可以实现有且仅有一个实例static修饰的静态成员可以满足该类有且只有一个,所有的对象都共享这一个静态成员。

对于(3)的理解,必须向外部系统提供唯一的公有访问方法。

单例模式代码实现

下面将详细介绍单例模式的8种具体写法,同时介绍各种写法的优缺点以及使用建议。

饿汉式-静态常量

这个是最简单、属于线程安全的饿汉式实现方式,在类加载的时候就创建了实例,由JVM保证线程安全。缺点就是可通过反射来创建新的实例,这个笔者会在后面给出一种解决方案。

public class Singleton1 {
    private static final Singleton1 instance = new Singleton1();

    private Singleton1(){};

    public static Singleton1 getInstance(){
        return instance;
    }
}

饿汉式-静态代码块

既然使用静态常量可以实现,那么使用静态代码块也是可以的,这两种实际上算是一种创建方式:

public class Singleton2 {
    private Singleton2(){};

    private static Singleton2 instance;

    static {
        instance = new Singleton2();
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}

这两种方式的优点就是写法简单,在类装载的时候就完成了实例化,不会出现线程不安全等问题;缺点是没有实现懒加载,类在装载的时候就完成了实例化,如果这里实例化的对象自始至终都没使用,那么将占用内存,造成内存浪费。

「建议:这种方式可以使用,但可能造成内存浪费。」

懒汉式---单个null检查

这种是采用懒汉式最简单的写法,不过这种方式是线程不安全的,在多线程环境下系统中可能存在多个实例,同样这个也可以通过反射创建新的实例:

public class Singleton3 {
    private Singleton3(){};

    private static Singleton3 instance;

    public static Singleton3 getInstance(){
        if(null==instance){
            instance = new Singleton3();
        }
        return instance;
    }
}

这种方式的优点就是实现了懒加载要求;缺点是只能在单线程环境中使用。在多线程环境下,如果某个线程执行到了if(null==instance)这一语句块,还未来的及往下执行,此时另一个线程也执行到了这个语句块,那么就会产生多个实例,因此在多线程环境下使用这种方式是不安全的。

「建议:实际开发过程中不使用这种方法,会出现线程不安全问题。」

懒汉式--单个null检查+同步锁

针对单个null检查的懒汉式创建方式的线程不安全问题,我们可以将实例化对象的过程放到同步代码块中:

public class Singleton4 {
    private Singleton4(){};
    private static Singleton4 instance;

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

但是这种方式并不能解决线程安全问题。假设现在有两个线程,线程A和线程B在并发访问getInstance方法,当线程A执行完if(null==instance)这一语句块且instance为null时,CPU执行权被线程B抢去了,此时线程A还没有创建实例。之后线程B也执行完if(null==instance)这一语句块,此时instance还是为null,然后就执行创建实例的逻辑。接着线程A再次获取了CPU的执行权,然后从之前中断的代码处继续执行,也会创建一个实例,这样系统中就有了两个对象。

如果我们将同步方式从创建对象这一过程提升到获取实例呢?这样问题就能解决,不过性能却大大降低了。你知道的在大部分情况下,线程都是获取实例,而不是创建实例,但是此处由于在getInstance方法上添加了synchronized关键字,使得现在线程获取实例都要排队:

public class Singleton4 {
    private Singleton4(){};
    private static Singleton4 instance;

    public synchronized static Singleton4 getInstance(){
        if(null==instance){
            instance = new Singleton4();
        }
        return instance;
    }
}

这种方式的优点就是解决了多线程环境下的线程不安全问题;缺点是代码执行效率低下,每个线程在获取对象实例的时候都要进行排队。

「建议:实际开发过程中不使用这种方法,执行效率很低。」

懒汉式---两个null检查+同步锁

其实这种方式和“单个null检查+同步锁”的懒汉式创建方式原理差不多,它所做的改变就是在同步块中再次判断对象是否存在,毫无疑问这种方式会引发空指针异常:

public class Singleton5 {
    private Singleton5(){};

    private static Singleton5 instance;

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

为什么会引发空指针异常呢?假设现在有两个线程,线程A和线程B在并发访问getInstance方法,当线程A获取锁之后,正在创建对象(创建对象需要经过一系列的步骤,编译器优化会发生指令重排序),且对象已经创建完成,但是对象的属性还没有进行初始化。此时线程B执行到了同步块中的if(null==instance)这一语句块处,尽管线程A创建的对象并不为空,但是它的属性还没完成初始化,此时如果线程B访问了线程A所创建对象的属性,那么就会出现空指针异常。

「建议:实际开发过程中不使用这种方法,会引发空指针异常。」

懒汉式---两个null检查+同步锁+volatile

“两个null检查(double-check)+同步锁”的懒汉式创建方式会引发空指针异常,问题出现线程A创建的对象上,如果我们禁止JVM指令重排,那么问题就解决了,此时可以使用volatile关键字。

public class Singleton6 {
    private static volatile Singleton6 instance;

    public Singleton6(){};

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

这种方式几乎没有缺点,它避免了程序在多线程环境下线程不安全问题的产生,同时也是一种延迟加载的懒汉式方式实现,效率较高。“双重检查+同步锁”这一组合方式在多线程开发中经常使用得到,通过两次if(null==instance)检查,保证了线程的安全。同时volatile关键字不仅禁止了JVM指令的重排序,还能保证对象的可见性。实际上这里实例化对象的代码只执行了一次,后续调用getInstance方法判断instance对象是否为null的时候,会直接返回实例化对象,避免了频繁的创建实例化对象。

初次接触可能比较困惑,为什么这里需要两次null检查(double-check),原因如下:第一个check为了提升性能。实例一旦被创建,那么所有的null检查返回的都是假的。因此如果你去掉第一个check,其实并不影响程序的执行,只是性能下降了,变成和直接同步方法一样了。第二个check是为了保证多线程环境下只创建一个实例(线程安全)。说白了第一个check多个线程可以同时进入,但是第二个check线程只能排队进入。

「建议:实际开发过程中推荐使用这种方法,性能、延迟加载和线程安全都有保证。」

懒汉式---静态内部类

这是一种线程安全的懒汉式创建对象方式,实例只在你第一次访问的时候才会生成,建议使用这种方式替代前面的double-check方式。

public class Singleton7 {
    private Singleton7(){};

    private static class InstanceHolder{
        private final static Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance(){
        return InstanceHolder.INSTANCE;
    }
}

这种方式的优点:(1)采用类的装载机制来保证初始化实例时只有一个线程;(2)内部类InstanceHolder在外部内被加载的时候并不会被立即实例化,只有在调用getInstance方法时才会被加载,且只加载一次。(3)类的静态属性只会在第一次加载类的时候初始化,因此JVM保证了线程的安全性,因为在类进行初始化时,其它线程是无法进入的。(4)利用静态内部类特点实现了延时加载,执行效率高。

「建议:实际开发过程中推荐使用这种方法,性能、延迟加载和线程安全都有保证。」

枚举

前面介绍的所有方法都有一个共同的问题,就是可通过反射方式来创建多个实例。如果你的类实现了序列化,那么就要防止在序列化时生成多个实例,为了解决这个问题?你可以使用枚举,枚举这种方式保证了线程安全、反射安全和序列化安全,只是在实际开发过程中这种方式用的较少:

public class Singleton8 {
    private Singleton8(){};

    private enum Singleton{
        INSTANCE;

        private final Singleton8 instance;

        Singleton(){
            instance = new Singleton8();
        }
        public Singleton8 getInstance(){
            return instance;
        }
    }
    public static Singleton8 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
}

「建议:实际开发过程中推荐使用这种方法,线程、反射和序列化安全都有保证。」

总结

在单例模式中,创建对象的方式总的来说分为两大类:饿汉式和懒汉式,无论是哪种,我们都要遵循这三个步骤:(1)只提供私有的构造方法;(2)只含有一个该类的静态私有对象;(3)提供一个静态的公有方法用于创建、获取静态私有对象。

饿汉式和懒汉式这两者最大的区别在于创建实例的时机,饿汉式以空间换时间,在初始化类的时候就创建对象,如果该对象自始至终没有使用,那么将浪费所占用的内存。懒汉式以时间换空间,只有对象在需要使用的时候才会创建,毫无疑问这种方式会影响程序的性能,更重要的是在多线程环境下,懒汉式这一方式是线程不安全的。

最后建议大家在实际开发过程中使用后面三种方式,尤其是基于静态内部类的懒汉式创建方式,如果经常用在序列化环境中,建议使用枚举这一方式。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 啃饼思录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 单例模式
    • 定义
      • 使用场景
        • 核心点
          • 要点
            • 实现
            • 单例模式代码实现
              • 饿汉式-静态常量
                • 饿汉式-静态代码块
                  • 懒汉式---单个null检查
                    • 懒汉式--单个null检查+同步锁
                      • 懒汉式---两个null检查+同步锁
                        • 懒汉式---两个null检查+同步锁+volatile
                          • 懒汉式---静态内部类
                            • 枚举
                              • 总结
                              相关产品与服务
                              文件存储
                              文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档