谈谈我对单例模式的理解

单例模式

定义:
一个类只允许创建一个对象(实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

为什么需要单例类?
好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免对象被频繁的创建与销毁,耗费系统资源。

单例模式的设计要素
一个私有构造函数 (确保只能单例类自己创建实例)
一个私有静态变量 (确保只有一个实例)
一个公有静态函数 (暴露给使用者)

六种单例模式实现方式

自己的一些思考

1、单例类为什么用final修饰?
final 用来修饰一个类:此类不能被继承,可以防止子类重写其中的方法导致单例被破坏

2、构造器为什么设置为私有?
设为私有才能防止外部类擅自调用,随意创建类对象

3、如何理解单例模式中的唯一性?
定义中提到,“一个类只允许创建一个对象”
那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?
我理解是后者,即单例模式创建的对象是进程唯一
这也就说,单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的

进程唯一和线程唯一:
“进程唯一”指的是进程内唯一,进程间不唯一。
“线程唯一”指的是线程内唯一,线程间可以不唯一,那么进程内就不唯一了

如何实现线程唯一的单例?
我们通过一个 map来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象

饿汉式

class Singleton{
    private static final Singleton instance=new Singleton();
    private Singleton(){}
    public static Singleton GetInstance(){
        return instance;
    }
}
class Singleton{
private:
    static Singleton instance;
    Singleton(){}
    ~Singleton(){}
public:
    static Singleton getInstance(){
        return instance;
    }
};

方法评价:

  • 线程安全
    提前实例化好了一个实例,避免了多个线程同时实例化造成线程不安全
  • 资源浪费
    不支持延迟加载,造成一定的资源浪费
    不过在有些情况下也有其优点,比如如果资源本身就不够,饿汉式可以在程序启动的时候触发报错,早日发现问题,我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃。总之就是有问题及早暴露

懒汉式

class Singleton{
    private static  Singleton instance;
    private Singleton(){}
    public static synchronized Singleton GetInstance(){
       if (instance==null){
           instance=new Singleton();
       }
       return instance;
    }
}

c++版:

class Singleton{
private:
    static Singleton* instance;
    Singleton(){}
    ~Singleton(){}
public:
    static Singleton* getInstance(){
        if(instance==nullptr){
            instance=new Singleton();
        }
        return instance;
    }
};

方法评价:

  • 可以延迟加载,节约了系统资源。
  • 每次获取单例的时候,是加锁的,如果频繁使用,会影响调用效率。

Double Check双重检查

饿汉式线程安全但不支持延迟加载,懒汉式延迟加载但是效率不高
双重检查则是一种既线程安全,又支持延迟加载,并且效率还高的单例实现方式

class Singleton{
    //volatile保证可见性、禁止instance=new Singleton()的指令重排序、但是不保证原子性
    private static volatile  Singleton instance;
    private Singleton(){}
    public static  Singleton GetInstance(){
       if (instance==null){
          synchronized (Singleton.class){
              if (instance==null){
                  instance=new Singleton();
              }
          }
       }
       return instance;
    }
}

方法评价:
既能保证线程安全,也能延迟加载节约资源开销

两次判空的目的分别是?

  • 第一次判空是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。

  • 第二次判空是防止二次创建实例,假如有一种情况,当instance还未被创建时,线程t1调用getInstance方法,顺利通过第一个判空,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于instance并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例instance。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1也会创建一个instance实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。

为什么使用volatile 修饰了instance 还用synchronized 锁?
因为 volatile 不保证原子性,为了保证instance= new Singleton()的原子性,使用synchronized

为什么要加volatile?——禁止指令重排序
由于指令重排序的原因,instance=new Singleton()这行代码的执行可能不如我们所愿

我们先看未被编译器优化即没有指令重排序的情况:

- 指令1:分配一款内存M

- 指令2:在内存M上初始化对象

- 指令3:将M的地址赋值给instance变量

但是编译器很可能出于性能考虑会进行指令重排序,交换指令2和3的顺序,编译器优化后的操作指令如下:

- 指令1:分配一块内存S

- 指令2:将M的地址赋值给instance变量

- 指令3:在内存M上初始化对象

现在有2个线程,刚好执行的代码被编译器优化过,过程如下:
在这里插入图片描述

最终线程B获取的instance是没有初始化的,此时去使用instance就会报空指针异常
那到底怎么办?
—— java中的volatile可以禁止指令重排!

静态内部类(推荐)

class Singleton{
    private Singleton(){}
    private static  class SingletonWrapper{
        static final Singleton instance=new Singleton();
    }
    public static  Singleton GetInstance(){
        return SingletonWrapper.instance;
    }
}

方法评价:
不存在多线程安全性问题
满足延迟加载
当外部类 Singleton被加载的时候,并不会创建 SingletonWrapper实例对象。只有当调用 getInstance() 方法时,才会创建 instance(延迟加载)

其实静态内部类这种方式是非常推荐使用的!简单高效。

枚举类

enum  Singleton {
    INSTANCE;
    public Singleton getInstance(){
        return INSTANCE;
    }
}

方法评价:
在java里面,用枚举类实现单例是最简单高效且功能完善的方式,天然的保证线程安全、天然的防止反序列化破坏单例

如何保证线程安全的?
虚拟机在加载枚举类的时候,会使用ClassLoader的loadClass方法,而这个方法使用锁保证了线程安全

为什么防止反序列化?
普通Java类在反序列化过程中,即使单例中构造函数是私有的,也会被反射给破坏掉。会被反射调用类的默认构造函数来初始化一个新对象。所以这就破坏了单例。
而枚举类型在序列化时,Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。所以并没有创建新对象,所以enum天然防止反序列化破坏单列。

总结

饿汉式
饿汉式的实现方式,在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。

懒汉式
懒汉式相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈

双重检测
双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。

静态内部类
静态内部类利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单,十分推荐的实现方式。

枚举
最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性,还天然防止反序列化。

如何选择那种单例模式呢?

1、在所有单例写法中,如果程序不是太复杂,单例对象又不多,推荐使用饿汉式单例。

2、但如果经常发生多线程并发情况下,推荐使用静态内部类和枚举式单例,

破坏单例的情况

我把可能出现单例被破坏的情况,一共归纳为五种,分别为:
多线程破坏单例、
指令重排破坏单例、
克隆破坏单例、
反序列化破坏单例、
反射破坏单例

第一种:多线程破坏单例
在多线程环境下,线程的时间片是由CPU自由分配的,具有随机性,而单例对象作为共享资源可能会同时被多个线程同时操作,从而导致同时创建多个对象。
当然,这种情况只出现在懒汉式单例中。如果是饿汉式单例,在线程启动前就被初始化了,不存在线程再创建对象的情况。

两种解决方案:

1、改为DCL双重检查锁的写法。

2、使用静态内部类的写法,性能更高。

第二种,指令重排破坏单例
Double Check双重检查中已经介绍了

第三种:克隆破坏单例
解决方案:我们可以在单例对象中重写clone() 方法,将单例自身的引用作为返回值。这样,就能避免这种情况发生。

第四种:反序列化破坏单例
反序列化会重新分配内存,创建新对象。破坏单例
解决方案:在反序列的过程中,Java API会调用readResolve()方法
只需要重写readResolve()方法,将返回值设置为已经存在的单例对象,就可以保证反序列化以后的对象是同一个了。之后再将反序列化后的对象中的值,克隆到单例对象中。

第五种:反射破坏单例
反射机制是可以拿到对象的私有的构造方法,所以反射可以任意调用私有构造方法创建单例对象。
解决方案:
第一种方案是在所有的构造方法中第一行代码进行判断,检查单例对象是否已经被创建,如果已经被创建,则抛出异常。这样,构造方法将会被终止调用,也就无法创建新的实例。

第二种方案,将单例的实现方式改为枚举式单例,因为在JDK源码层面规定了,不允许反射访问枚举。

单例的典型使用场景

1、在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。

2、表示文件系统的类,一个操作系统一定是只有一个文件系统,因此文件系统的类的实例只有一个。

3、再比如打印机打印程序的实例,一台计算机可以连接好几台打印机,但是计算机上的打印程序通过单例模式来避免两个打印作业同时输出到打印机。

4、spring框架中的bean,默认就是单例的

到底该不该使用单例模式

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接调用getInstance()就可以了。但是这种使用方法有点类似硬编码,会带来诸多问题,比如单例不支持有参数的构造函数,对代码的扩展性不友好等等

有人把单例当作反模式,主张杜绝在项目中使用。我个人觉得这有点极端。
模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

C++里如何实现单例模式

著名的《Effective C++》系列书籍的作者 Meyers 提出了C++ 11版本最简洁的跨平台方案,即Meyers’ Singleton
实现如下:

class Singleton {
private:
    Singleton() {}

    ~Singleton() {}

public:
    static Singleton getInstance() {
        static Singleton instance; //c++11下,局部静态对象初始化在多线程下是安全的
        return instance;
    }
};

这样的写法即简洁又完美!需要注意的是此写法需要支持C++11以上、GCC4.0编译器以上。
参考文章

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值