01 背景
以我们常用办公软件WPS为例,我们使用的时候期望点击一次工具栏弹出一个对话框,再次点击的时候仍然是当前对话框,而不是出现多个对话框。反映到编程中,其实就是对话框只被实例化一次,这就是单例模式的一个应用场景。
02 概述
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
根据定义可知单例模式的要点有三个:
1、某个类只能有一个实例;
2、它必须自行创建这个实例;
3、它必须自行向整个系统提供这个实例。
单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
03 实现
3.1 饿汉式
饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。一上来就先实例化,但若类没有使用的话,就有点浪费资源类。
优点:这样做的好处是编写简单,无需关注线程安全问题。
缺点:
1、会生产出过多的实例对象,无论你是否要使用他们。
2、无法做到延迟创建对象,但是我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载,所以需要懒汉法。
3.2 懒汉式
饿汉式(又称饱汉模式),很饱不着急,延迟加载,啥时候用啥时候创建实例,存在线程安全问题。
实例在开始时为空,第一次加载后才实例化。可节约一些资源,但在并发时有可能出现多个单例。
优点:延时加载,用的时候才会生产对象。
缺点:存在线程安全问题,需要保证同步,付出效率的代价。
1、单线程实现
这种写法是最简单的,由私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null判断,如果是null就new一个出来,最后返回singleton对象。
优点:延迟加载,资源利用率高,不执行getInstance()就不会被实例化,可以执行该类的其他静态方法。
缺点:第一次加载时不够快,线程不安全,多线程使用不必要的同步开销大。
2、多线程实现
1)直接加锁
优点:线程安全。
缺点:代价太高(整个判空和申请阶段全部都加锁,一劳永逸,但是效率低,锁范围太大)。
2)双检查锁
双重锁模式,是饱汉模式的优化,进行双重判断,当已经创建过实例对象后就无需加锁,提高效率。也是一种推荐使用的方式。
只有对象为空的时候才加锁,加完锁后再判空,防止在加锁的过程中被另一个线程调用new,即进行双检查:第一次检查是避免代价过高的问题,第二次检查是防止多线程问题。
注:可以看出来与直接加锁相比,锁的范围缩小了,效率得以提升,同时通过在申请实例前加锁保证线程安全。
优点:比直接加锁效率高。
缺点:内存读写reorder不安全。
reorder问题:一般new的执行过程认为是分配内存->构造函数初始化->返回地址,但是实际上可能是分配内存->返回地址->构造函数这种错乱的顺序。如果线程1reorder,另外一个线程2判断非空直接返回pSingleton,但是这个对象实例是无法正常使用的,它只是一个还未调用构造函数初始化的内存。
所以编译器需要解决这类问题,即编译器不能优化。
3)volatile
volatile这个关键字有两层语义:
第一层语义是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中,即看到的都是最新的结果。
第二层语义是禁止指令重排序优化。我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。
3.3 静态内部类
有没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的:
3.4 选择
一般采用饿汉式,若对资源十分在意可以采用静态内部类,不建议采用懒汉式及双重检测。
04 特点
4.1 优点
1、提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
2、只存在一个对象,节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
4.2 缺点
1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
3、滥用单例将带来一些负面问题。现在很多面向对象语言(如Java)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
05 应用场景
在以下情况下可以使用单例模式:
1、系统只需要一个实例对象(线程池、缓存、硬件设备等),或者需要考虑资源消耗太大而只允许创建一个对象。
2、客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
3、一个具有自动编号主键的表可以有多个用户同时使用,但数据库中只能有一个地方分配下一个主键编号,否则会出现主键重复,因此该主键编号生成器必须具备唯一性,可以通过单例模式来实现。