跳至主要內容

【设计模式】创建型-③单例模式

holic-x...大约 10 分钟设计模式设计模式

【设计模式】创建型-③单例模式

学习核心

  • 单例模式核心
    • 单例模式的基本概念、应用场景
    • 单例模式的实现(懒汉式、饿汉式、双检锁、枚举,版本演进)
    • 单例模式邪恶论
    • 基于单例模式案例分析
  • 业务场景案例
    • 【Spring框架】:Spring框架创建单例对象(面向无状态服务)
    • 【业务开发相关】:一些工具类(xxxUtils)、全局属性(constant)、枚举(enums)相关定义

单例模式构建图示

​ 从懒加载、线程安全等方面结合场景应用进行分析(单例模式的实现演进),选择较为合适的方式构建单例

image-20220201231734900

基本概念

​ 单例模式又叫做单态模式或者单件模式。在 GOF 书中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式中的“单例”通常用来代表那些本质上具有唯一性的系统组件(或者叫做资源)。比如文件系统、资源管理器等等。

​ 单例模式的目的就是要控制特定的类只产生一个对象,当然也允许在一定情况下灵活的改变对象的个数

单例模式类图参考如下:

image-20220202105921046

​ 单例模式可分为有状态的和无状态的。有状态的单例对象一般也是可变的单例对象,多个单态对象在一起就可以作为一个状态仓库一样向外提供服务。没有状态的单例对象也就是不变单例对象,仅用做提供工具函数。

场景案例

1>数据库的连接池不会反复创建

2>spring中⼀个单例模式bean的⽣成和使⽤

3>日常的代码中需要设置全局的的⼀些属性保存

单例模式的实现:

​ 一个类的对象的产生是由类构造函数来完成的,如果想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使 得外面的类不能通过引用来产生对象;同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法。

1>定义全局的静态私有变量
2>构造函数私有化
3>提供公共方法供外部访问类属性

模式应用

​ 单例模式的实现⽅式较多,主要在实现上是否⽀持懒汉模式、是否线程安全中运⽤各项技巧。也有⼀些场景不需要考虑懒加载也就是懒汉模式的情况,会直接使⽤ static 静态类或属性和⽅法的⽅式进⾏处理,供外部调⽤

00.静态类使用

/**
 * 00.静态类使用
 * 第一次运行的时候直接初始化Map
 */
public class Singleton_00 {

    public static Map<String,String> cache = new ConcurrentHashMap<String, String>();

}

01.懒汉模式(线程安全、线程不安全)

​ 懒汉模式(线程不安全)的实现满足了懒加载,但如果同时有多个访问者去获取对象实例,则有可能造成多个同样的实例并存,从而没有达到单例的要求

/**
 * 懒汉模式(线程不安全)
 */
public class Singleton_01 {

    // 构建一个静态的全局的变量供内部调用
    private static Singleton_01 instance;

    // 构造函数私有化
    private Singleton_01() {
    }

    // 对外提供公共接口
    public static Singleton_01 getInstance(){
        // 懒汉模式:在调用的时候进行判断,如果实例为null则构建
        if (null != instance) {
            return instance;
        }
        return new Singleton_01();
    }

}

​ 懒汉模式(线程安全)的实现满足线程安全,但由于将锁加到方法上,所有的访问都因需要锁占用从而导致资源的浪费

/**
 * 懒汉模式(线程安全)
 */
public class Singleton_02 {

    // 构建一个静态的全局的变量供内部调用
    private static Singleton_02 instance;

    // 构造函数私有化
    private Singleton_02() {
    }

    // 对外提供公共接口:将锁加到方法上,满足线程安全,所有访问需要锁占用
    public static synchronized Singleton_02 getInstance(){
        // 懒汉模式:在调用的时候进行判断,如果实例为null则构建
        if (null != instance) {
            return instance;
        }
        return new Singleton_02();
    }

}

02.饿汉模式(线程安全)

​ 程序启动的时候直接运行加载(初始化实例),提供方法供外部调用。非懒加载概念

​ 可能导致的问题:例如下载游戏软件,游戏地图还没有打开,程序自动将地图全部实例化,最明显的体验就是还没开始玩游戏就内存炸裂

/**
 * 饿汉模式(线程安全)
 */
public class Singleton_03 {

    // 构建一个静态的全局变量,并进行初始化
    private static Singleton_03 instance = new Singleton_03();

    // 构造函数私有化
    private Singleton_03() {
    }

    // 对外提供接口访问实例
    public static Singleton_03 getInstance() {
        return instance;
    }

}

03.使用类的内部类(线程安全)-推荐

​ 使⽤类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的⽅式耗费性能

​ JVM虚拟机可以保证多线程并发访问的正确性,即⼀个类的构造⽅法在多线程环境下可以被正确的加载

/**
 * 使⽤类的内部类(线程安全)
 */
public class Singleton_04 {

    // 构建静态内部类实现单例模式
    private static class SingletonHolder {
        private static Singleton_04 instance = new Singleton_04();
    }

    // 构造函数私有化
    private Singleton_04() {
    }

    // 对外提供接口访问实例
    public static Singleton_04 getInstance() {
        return SingletonHolder.instance;
    }

}

04.双重锁校验(线程安全)

​ 双重锁检验的方式,在满足懒加载的同时采用⽅法级锁的优化,减少了部分获取实例的耗时

/**
 * 双重锁校验(线程安全)
 */
public class Singleton_05 {

    private static volatile Singleton_05 instance;

    private Singleton_05() {
    }

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

}

05.CAS「AtomicReference」(线程安全)

​ java并发库提供了很多原⼦类来⽀持并发访问的数据安全性: AtomicIntegerAtomicBooleanAtomicLongAtomicReferenceAtomicReference可以封装引⽤⼀个V实例,下述⽀持并发访问的单例模式就是使⽤了这样的⼀个特点

​ 使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以⽀持较⼤的并发性。CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中

/**
 * CAS「AtomicReference」(线程安全)
 */
public class Singleton_06 {

    private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();

    private static Singleton_06 instance;

    private Singleton_06() {
    }

    public static final Singleton_06 getInstance() {
        for (; ; ) {
            Singleton_06 instance = INSTANCE.get();
            if (null != instance) return instance;
            INSTANCE.compareAndSet(null, new Singleton_06());
            return INSTANCE.get();
        }
    }

    public static void main(String[] args) {
        // org.itstack.demo.design.Singleton_06@2b193f2d
        System.out.println(Singleton_06.getInstance()); 
        // org.itstack.demo.design.Singleton_06@2b193f2d
        System.out.println(Singleton_06.getInstance()); 
    }

}

06.枚举单例-推荐(Effective Java)

​ Effective Java作者推荐使⽤枚举的⽅式解决单例模式,这种方式解决了“线程安全、自由串行化、单一实例”等问题

public enum Singleton_07 {
    INSTANCE;
    public void test(){
        System.out.println("hi~");
    }

}

4.扩展概念

单例模式邪恶论

​ 单例模式在 java 中的使用存在很多陷阱和假象,如果没有意识到单例模式使用局限性,往往会在在系统中布下隐患

​ 多个虚拟机 :当系统中的单例类被拷贝运行在多个虚拟机下的时候,在每一个虚拟机下都可以创建一个实例对象。在使用了 EJB、JINI、RMI 技术的分布式系统中,由于中间件屏蔽掉了分布式 系统在物理上的差异,所以想知道具体哪个虚拟机下运行着哪个单例对象是很困难的。 因此,在使用以上分布技术的系统中,应该避免使用存在状态的单例模式,因为一个有状态的单例类,在不同虚拟机上,各个单例对象保存的状态很可能是不一样的,问题也就随之产生。而且在 EJB 中不要使用单例模式来控制访问资源,因为这是由 EJB 容器来负责的。 在其它的分布式系统中,当每一个虚拟机中的资源是不同的时候,可以考虑使用单例模式来 进行管理。

​ 多个类加载器:当存在多个类加载器加载类的时候,即使它们加载的是相同包名,相同类名甚至每个字节都完全相同的类,也会被区别对待的。因为不同的类加载器会使用不同的命名空间 (namespace)来区分同一个类。因此,单例类在多加载器的环境下会产生多个单例对象。也许你认为出现多个类加载器的情况并不是很多。其实多个类加载器存在的情况并不少见。在很多 J2EE 服务器上允许存在多个 servlet 引擎,而每个引擎是采用不同的类加载器的; 浏览器中 applet 小程序通过网络加载类的时候,由于安全因素,采用的是特殊的类加载器, 等等。 这种情况下,由状态的单例模式也会给系统带来隐患。因此除非系统由协调机制,在一 般情况下不要使用存在状态的单例模式。

​ 错误的同步处理:在使用上面介绍的懒汉式单例模式时,同步处理的恰当与否也是至关重要的。不然可能 会达不到得到单个对象的效果,还可能引发死锁等错误。因此在使用懒汉式单例模式时一定 要对同步有所了解。不过使用饿汉式单例模式就可以避免这个问题。

​ 子类破坏了对象控制:如果类构造函数变得不再私有,就有可能失去对对象的控制。这种情况只能通过良好的文档来规范。

​ 串行化(可序列化):为了使一个单例类变成可串行化的,仅仅在声明中添加“implements Serializable”是不够的。因为一个串行化的对象在每次返串行化的时候,都会创建一个新的对象,而不仅仅是一个对原有对象的引用。为了防止这种情况,可以在单例类中加入 readResolve 方法。关于这个方法的具体情况请参考《Effective Java》一书第 57 条建议。 其实对象的串行化并不仅局限于上述方式,还存在基于 XML 格式的对象串行化方式。 这种方式也存在上述的问题,所以在使用的时候要格外小心。

​ 上面罗列了一些使用单例模式时可能会遇到的问题。而且这些问题都和 java 中的类、 线程、虚拟机等基础而又复杂的概念交织在一起,在使用的时候要结合应用场景考虑周全。

​ 抛开单例模式,使用下面一种简单的方式也能得到单例,而且如果你确信此类永远是单例的,使用下面这种方式也许更好一些。

public static final Singleton INSTANCE = new Singleton(); 

​ 而使用单例模式提供的方式,这可以在不改变 API 的情况下,改变我们对单例类的具体要求。

场景案例

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3