设计模式笔记——(一:单例模式 Singleton Pattern)

本文深入解析单例模式的原理及其实现方式,包括饿汉式与懒汉式单例,并介绍InitializationOnDemandHolder技术,旨在帮助读者理解如何在Java中高效实现单例模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

实际开发中,我们经常遇到这样的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。

简单实现

public class SingletonPattern {
    // 私有静态属性
    private static SingletonPattern mSingletonPattern;

    /**
     * 私有构造方法,外部无法创建该对象
     */
    private SingletonPattern() {
    }

    /**
     *  public方法,以便供外界其他对象使用
     *  static关键字,静态方法,类外直接通过类名来访问,无须创建对象
     *  事实上在类外也无法创建TaskManager对象,因为构造函数是私有的
     * @return
     */
    public static SingletonPattern getInstance() {
        if (null == mSingletonPattern) {
            mSingletonPattern = new SingletonPattern();
        }
        return mSingletonPattern;
    }
}

以上代码是最简单基础的单例模式的写法,相信大家都见过或者自己写过,我们稍微解释一下。

  1. 由于每次使用new关键字来实例化SingletonPattern类时都将产生一个新对象,为了确保SingletonPattern实例的唯一性,我们需要禁止类的外部直接使用new来创建对象,因此需要将SingletonPattern的构造函数的可见性改为private(可见性只对类外有效)。
  2. 类的外部无法再使用new来创建对象,我们需要在SingletonPattern中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在SingletonPattern中定义一个静态的SingletonPattern类型的私有成员变量。
  3. 最后一步我们需要在SingletonPattern中实现初始化这个单例对象以及提供这个对象给外部使用的方法getInstance(),首先判断mSingletonPattern对象是否存在,如果不存在,则使用new关键字创建对象,再返回该对象,否则直接返回已有的对象,从而确保实例对象的唯一性。

上述代码也是单例模式的一种最典型实现方式,通过它我们理解单例模式的定义和结构就非常容易了。

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

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

单例模式是结构最简单的设计模式之一,在它的核心结构中只包含一个被称为单例类的特殊类。

饿汉式单例与懒汉式单例

事实上,单例模式又分饿汉式与懒汉式,我们上面的例子就是懒汉式,我们再来看一下饿汉式单例。

public class SingletonPattern {
    /**
     * 类被加载的时候就创建了对象
     * 外部要使用该对象时直接返回该对象
     * 不再需要判断对象是否存在以及对象的初始化,因为已经初始化了
     */
    private static SingletonPattern mSingletonPattern = new SingletonPattern();

    private SingletonPattern() {
    }

    public static SingletonPattern getInstance() {
        return mSingletonPattern;
    }
}

对比这两种单例模式,我们能发现它们的区别在于对象在什么时候创建(饿汉式,就像人很饿的时候等不及接收能吃饭的通知就自己先吃了;懒汉式,就像很懒的人,你不指令要他去做家务他是永远不会去做的。),两种方式的比较如下:

  1. 饿汉式单例,在一开始就创建了对象,即使我们可能一直都不会去调用它,这样会浪费系统资源,而且若是程序有多个饿汉式单例,且他们的初始化过程耗时长则程序的加载时间会加长,但使用过程中的调用速度和反应时间上会有优势。而懒汉式单例,仅在需要使用到该对象的时候才去初始化,无须一直占用系统资源,实现了延迟加载。
  2. 懒汉式单例,在调用对象的时候才创建对象,若创建对象的过程耗时较长,多线程的情况下将会发生错误(A线程要调用该单例对象,进入方法判断该对象为null,则进入初始化,此时B线程也需要调用该对象,发现该对象已经不等于null,则会返回该对象用于操作,但是此时该对象并没有初始化完成,将会发生异常。)。而饿汉式单例的优点就在于无需考虑多线程访问问题,可以确保实例的唯一性。

基于懒汉式的多线程访问问题,有人想到了一个双重检查锁定的办法来解决。使用synchronized关键字给创建实例的代码加锁,且在加锁代码内外均判断一次该对象是否为空。

public class SingletonPattern {

    private volatile static SingletonPattern mSingletonPattern;

    private SingletonPattern() {
    }

    public static SingletonPattern getInstance() {
        //第一重判断
        if (mSingletonPattern == null) {
            //锁定代码块
            synchronized (SingletonPattern.class) {
                //第二重判断
                if (mSingletonPattern == null) {
                    mSingletonPattern = new SingletonPattern(); //创建单例实例
                }
            }
        }
        return mSingletonPattern;
    }
}

需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此使用双重检查锁定来实现单例模式虽然能弥补多线程访问带来的问题,但也不是一种完美的实现方式。

更好的实现方式

饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。有没有一种方法能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。
我们在单例类中增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:

public class SingletonPattern {

    private SingletonPattern() {
    }

    /**
     * 内部私有静态类
     */
    private static class HolderClass {
        private final static SingletonPattern instance = new SingletonPattern();
    }

    public static SingletonPattern getInstance() {
        return HolderClass.instance;
    }
}

上述代码有点类似于将饿汉式单例与懒汉式单例整合在了一起,取各自的优点,去除各自的缺点。由于静态单例对象没有作为SingletonPattern的成员变量直接实例化,因此类加载时不会实例化SingletonPattern,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。

总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
主要优点
1. 单例模式提供了对唯一实例的受控访问。
2. 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
3. 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
主要缺点
1. 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
2. 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
3. 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
适用场景
1. 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
2. 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值