单例模式的几种实现方式

本文详细介绍了单例模式的定义、多种实现方式,包括静态内部类、枚举类、饿汉模式和懒汉模式,并探讨了延迟加载和单例模式在实际应用中的场景,如任务管理器、回收站、日志应用等。同时,文中还讨论了单例模式在并发环境下的线程安全问题以及Java中的优化策略。

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

Table of Contents

单例模式定义

单例模式的几种实现方式

静态内部类

枚举类实现方式

饿汉模式

懒汉模式

关于延迟加载

单例模式的实际应用


单例模式定义

单例模式是设计模式当中比较常见的一个软件设计模式。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类在整个系统当中只有一个对象实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。

单例模式的几种实现方式

单例模式要保证系统中只有一个实例,在单线程情况下实现比较轻松,但是考虑并发情况的话,则实现的时候就需要做特殊处理。

推荐的方式如下:

静态内部类


public class SingletonInnerStaticClass {
    //线程安全,静态内部类
    private SingletonInnerStaticClass(){

    }

    /*
    通过静态内部类来实现
    只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,
    从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
    只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance
    */

    private static class SingletonHolder{
        public static SingletonInnerStaticClass instance = new SingletonInnerStaticClass();
    }

    public static SingletonInnerStaticClass newInstance(){
        return SingletonHolder.instance;
    }
}

私有构造方法,且只有当程序中去调这个内部类时,JVM才会去加载这个单例对象,也保证了延迟加载。但是在遇到序列化对象时,默认的方式运行得到的结果是多例的。

问题:为什么这种方式是线程安全的?

//todo

枚举类实现方式

class Singleton {

}
//枚举类(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用
//只要 EnumSingleton.INSTANCE.getInstance() 即可获得所要实例
enum EnumSingleton {
    //有问题
    INSTANCE;

    private Singleton instance;

    EnumSingleton() {
        instance = new Singleton();
    }
    public Singleton getInstance() {
        return instance;
    }
}

饿汉模式

public class SingletonEager {
    //饿汉模式,eager Singleton,线程安全
    //instance在类装载时就实例化,这时候初始化instance显然没有达到lazy loading的效果。不推荐。
    /*
    一上来就把单例对象创建出来了,要用的时候直接返回即可,这种可以说是单例模式中最简单的一种实现方式。
    但是问题也比较明显。单例在还没有使用到的时候,初始化就已经完成了。
    也就是说,如果程序从头到位都没用使用这个单例的话,单例的对象还是会创建。
    这就造成了不必要的资源浪费。所以不推荐这种实现方式。
     */

    private SingletonEager(){

    }

    private static SingletonEager instance = new SingletonEager();//无人调用,主动实例化

    public static synchronized SingletonEager getInstance(){
        return instance;
    }

}

饿汉模式的问题在于会造成一定的资源浪费

以下的方式都有一些问题:

懒汉模式

public class Singleton1NotThreadSafe {
    //lazy loading,线程不安全,不推荐
    private Singleton1NotThreadSafe() {


    }

    private static Singleton1NotThreadSafe instance;

    //private static Singleton1 getInstance() {//需要定义为public供外部访问
    public static Singleton1NotThreadSafe getInstance() {

        if (instance == null) {
            //private 构造方法,在类的内部是可以调用的
            return new Singleton1NotThreadSafe();
        }

        return instance;
    }
}

虽然用延迟加载方式实现了懒汉式单例,但在多线程环境下会产生多个single对象,不推荐

通过加synchronized可以解决并发的问题。


public class SingletonThreadSafeButSlowLazyLoading {
    //lazy loading,线程安全,但是效率较慢,不推荐,同步锁锁的是对象,每次取对象都有加锁
    private SingletonThreadSafeButSlowLazyLoading() {

    }

    private static SingletonThreadSafeButSlowLazyLoading instance;

    public synchronized static SingletonThreadSafeButSlowLazyLoading getInstance() {

        if (instance == null) {
            //private 构造方法,在类的内部是可以调用的
            return new SingletonThreadSafeButSlowLazyLoading();
        }

        return instance;
    }
}

但效率较低,每一次取这个单例都是同步的操作。不推荐。

使用双重锁定:


public class SingletonDoubleCheck {
    /*
     加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,
     然而它存在着性能问题,依然不够完美。
     synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了
     */

    //Double CheckLock实现单例:DCL也就是双重锁判断机制(由于JVM底层模型原因,偶尔会出问题,不建议使用)
    private SingletonDoubleCheck(){

    }

    private static SingletonDoubleCheck instance = null;

    public static SingletonDoubleCheck getInstance() {
        if (instance == null) {//1
            synchronized (SingletonDoubleCheck.class) {
                if (instance == null) {//2
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}

虚拟机会对我们的代码自动进行一些优化,包括锁粗化,指令重排序等等,关于这个内容可以参考我的博客:[java] volatile关键字详解 。 带来的影响就是这里看上去加的两个锁在执行时会被虚拟机合并为一个锁。在JDK1.5之后,可以使用volatile变量禁止指令重排序,让DCL生效。

只需要将instance变量加上一个volatile关键字修饰即可:

private static volatile SingletonDoubleCheck instance = null;

额外需要指出的是,由于java提供反射机制,因此从技术层面来时,任何的单例都是有办法让它变为多例的。

关于延迟加载

延迟加载,也叫懒汉模式,英文:lazyloading。也有国外的程序员将它成为:INITIALIZATION ON DEMAND HOLDER。即按需初始化。随着实际的开发经验变多,博主对延迟加载的理解也更加的丰富。从后端来讲,如果使用java和spring作为开发框架的话,那么事实上java的虚拟机会做很多延迟加载的优化。而对于spring框架来说,它也提供了可以配置是否延迟加载一个bean的选项。spring单例的bean默认都是在启动时加载的,这也是我们为什么启动一个应用通常需要一定时间的原因之一。

但总的来说对于后端技术来讲,框架帮助程序员提供了很多延迟加载的优化。而延迟加载对于前端界面来说也是非常重要的技术。试想如果进入一个页面的时候,这个页面必须要加载完所有的东西才能加载出来,那会非常影响用户体验速度会很慢,而通过延迟加载我们可以按照需要再去加载一些资源。这样的话前端的效率会高很多。博主在实际开发的项目中就遇到了这样的例子

单例模式的实际应用

1. Windows的Task Manager(任务管理器)就是很典型的单例模式

2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。

4. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。

7. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

8. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

9. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值