单例模式

01
背景
   以我们常用办公软件WPS为例,我们使用的时候期望点击一次工具栏弹出一个对话框,再次点击的时候仍然是当前对话框,而不是出现多个对话框。反映到编程中,其实就是对话框只被实例化一次,这就是单例模式的一个应用场景。
02
概述
    单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

    根据定义可知单例模式的要点有三个:

    1、某个类只能有一个实例;

    2、它必须自行创建这个实例;

    3、它必须自行向整个系统提供这个实例。

    单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

03
实现
    3.1 饿汉式
    饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。一上来就先实例化,但若类没有使用的话,就有点浪费资源类。
public class Singleton {  private static instance = new Singleton();  private Singleton() {}  public static getInstance() {    return m_instance;  }}

    优点:这样做的好处是编写简单,无需关注线程安全问题。

    缺点:

    1、会生产出过多的实例对象,无论你是否要使用他们。

    2、无法做到延迟创建对象,但是我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载,所以需要懒汉法。

    3.2 懒汉式
    饿汉式(又称饱汉模式),很饱不着急,延迟加载,啥时候用啥时候创建实例,存在线程安全问题。

    实例在开始时为空,第一次加载后才实例化。可节约一些资源,但在并发时有可能出现多个单例。

    优点:延时加载,用的时候才会生产对象。

    缺点:存在线程安全问题,需要保证同步,付出效率的代价。

    1、单线程实现

    这种写法是最简单的,由私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null判断,如果是null就new一个出来,最后返回singleton对象。

public class Singleton {    private static Singleton singleton = null;//声明    private Singleton(){}    public static Singleton getSingleton() {        if(singleton == null) singleton = new Singleton();//实例化        return singleton;    }}

    优点:延迟加载,资源利用率高,不执行getInstance()就不会被实例化,可以执行该类的其他静态方法。

    缺点:第一次加载时不够快,线程不安全,多线程使用不必要的同步开销大。

    2、多线程实现

    1)直接加锁
Singleton* Singleton::getInstance() {  Lock lock;  if (nullptr == m_instance) {    m_instance = new Singleton();  }  return m_instance;}

    优点:线程安全。

    缺点:代价太高(整个判空和申请阶段全部都加锁,一劳永逸,但是效率低,锁范围太大)。

    2)双检查锁
    双重锁模式,是饱汉模式的优化,进行双重判断,当已经创建过实例对象后就无需加锁,提高效率。也是一种推荐使用的方式。

Singleton* Singleton::getInstance() {  if (nullptr == m_instance) {    Lock lock;    if (nullptr == m_instance) {      m_instance = new Singleton();    }  }  return m_instance;}

    只有对象为空的时候才加锁,加完锁后再判空,防止在加锁的过程中被另一个线程调用new,即进行双检查:第一次检查是避免代价过高的问题,第二次检查是防止多线程问题。

    注:可以看出来与直接加锁相比,锁的范围缩小了,效率得以提升,同时通过在申请实例前加锁保证线程安全。

    优点:比直接加锁效率高。

    缺点:内存读写reorder不安全。

    reorder问题:一般new的执行过程认为是分配内存->构造函数初始化->返回地址,但是实际上可能是分配内存->返回地址->构造函数这种错乱的顺序。如果线程1reorder,另外一个线程2判断非空直接返回pSingleton,但是这个对象实例是无法正常使用的,它只是一个还未调用构造函数初始化的内存。

    所以编译器需要解决这类问题,即编译器不能优化。

    3)volatile

std::atomic<Singleton*> Singleton::m_instance;std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {  Singleton* tmp = m_instance.load(std::memory_order_relaxed);  std::atomic_thread_fence(std::memory_order_acuire);//获取内存fence  if (nullptr == tmp) {    tmp = new Singleton;    std::atomic_thread_fence(std::memory_order_release);//释放内存fence    m_instance.store(tmp,std::memory_order_relaxed);  }  return tmp;}

    volatile这个关键字有两层语义:

    第一层语义是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中,即看到的都是最新的结果。

    第二层语义是禁止指令重排序优化。我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。

    3.3 静态内部类
    有没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的:

public class Singleton {    private static class Holder {          private static Singleton singleton = new Singleton();    }    private Singleton(){}      public static Singleton getSingleton(){          return Holder.singleton;  //静态类方式获取实例    }}

    3.4 选择

单例模式 懒汉模式 饿汉模式
概念 在类加载时不创建实例,采用延迟加载的方式,在运行调用时创建实例 在类加载的时候,就完成初始化
特点 类加载速度快,但是运行时获取对象的速度较慢(时间换空间)。 类加载较慢,但获取对象速度快(空间换时间)。
延迟加载 具备 不具备
线程安全 线程不安全 线程安全

    一般采用饿汉式,若对资源十分在意可以采用静态内部类,不建议采用懒汉式及双重检测。

04
特点

    4.1 优点

    1、提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。

    2、只存在一个对象,节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。

    4.2 缺点

    1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。

    2、单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

    3、滥用单例将带来一些负面问题。现在很多面向对象语言(如Java)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

05
应用场景
   在以下情况下可以使用单例模式:

    1、系统只需要一个实例对象(线程池、缓存、硬件设备等),或者需要考虑资源消耗太大而只允许创建一个对象。

    2、客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

    3、一个具有自动编号主键的表可以有多个用户同时使用,但数据库中只能有一个地方分配下一个主键编号,否则会出现主键重复,因此该主键编号生成器必须具备唯一性,可以通过单例模式来实现。

添加微信免费咨询高性价比云主机信息
微信号:kinnah333

未经允许不得转载:云技术 » 单例模式

赞 (0)