单例模式(Singleton)

本文探讨了单例模式在软件设计中的重要性,详细解释了简单单例模式的实现,以及在多线程环境下如何确保线程安全,提到了双重检查锁定和基于类初始化的解决方案,最后总结了不同单例模式的优缺点。

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

为什么需要单例模式

单例模式是自己最先接触的一种设计模式,当时还是开发C++的代码。当时的应用场景是一个控制台程序,对于一个管理资源的类,也涉及初始化、启动等,这样的类只适合构造一个实例,然后不断的复用,保证在运行进程内只有一个实例,便于管理;同时也能减少资源的开销。


从面向对象的概念上讲,我们知道封装,将相关联的东西封装成一个类。在Java中,一种最简单的模式就是POJO(或者叫Java Bean),包括一些属性和对应的getter和setter方法。这种是对属性的封装,通常是比较明显的对应一些实体,他们提供对数据的封装。另外的,就是更常见的类,因为面向对象编程,特别是在Java中,类似的操作或者语义都放在一个类中。


当需要使用类中的方法时,可以声明一个类的实例,每一个类的实例都拥有这个类的非static属性的一份拷贝,方法也是类似。通常的做法是,在需要用的地方声明一个实例,然后用实例去调用对应的方法。但是有的时候,希望这个类只存在一个实例,也就是这个类中的属性只有一份状态,在各处使用时都是对那一份数据的操作。


简单的单例模式

在Java中,常见的模式如下:

public class Singleton {

    //私有,静态的一个实例
    private static Singleton instance = new Singleton();

    // 必须得实现一个私有的无参构造函数,防止调用方直接new实例
    private Singleton() {}

    // 供使用者调用
    public static Singleton getInstance() {
        return instance;
    }
}

类的使用者要想使用这个类,只有一种方法得到这个类的实例;并且在类的内部,这个实例是静态的,只存在一份。因此,这就保证了“单例”。注意,这样的写法也是线程安全的。


但这中方法有一个问题,就是在类加载时就要初始化,如果如下初始化的内容比较多,加载起来就会比较慢,因此有了新的方法——延迟初始化(Lazy Initialize),如下:

public class Singleton {

    //私有,静态的一个实例,先不初始化
    private static Singleton instance;

    // 必须得实现一个私有的无参构造函数,防止调用方直接new实例
    private Singleton() {}

    // 供使用者调用
    public static Singleton getInstance() {
        if (instance != null) {
            instance = new Singleton();
        }
        return instance;
    }
}
但是这样又出现了一个问题,就是会导致线程不安全!


其他方案

但是常见的模式在并发环境中会出现问题。

当多个线程同时调用getInstance()方法时,有可能某个线程得到的instance还没有被初始化,这样将不会得到一个初始化好的实例。

首先想的的方法是加同步。


这个时候通常还结合另一种技术——延迟初始化,也就是不采用简单单例模式中的,在声明变量是就初始化的方式,而是等到要用时在进行初始化。

同时,还会加上双重检查,得到的通常模式如下:

class UnSafeSingleton {
    private static UnSafeSingleton instance;

    private UnSafeSingleton() {}

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

可以看到,这种方式加上了同步处理,并且进行了延迟初始化,稍微不注意,以为这个会是线程安全的。

但是,这边还是有问题,就是在给instance赋值时,还是会出现不一致的问题。


一种解决办法是,给instance属性加上volatile特征,这样保证instance对各个线程的可见性,保证只有一份存在。

另一种方法是采用基于类初始化的方法。JVM在类的初始化阶段(即在Class在被加载后,且被线程使用之前),会执行类的初始化。

在执行类的初始化期间,JVM回去获得一个锁。这个锁可以同步多个线程对同一个类的初始化。代码如下:

class SafeSingleton {

    private SafeSingleton() {}

    private static class SafeSingletonHolder {
        public static SafeSingleton intance = new SafeSingleton();
    }

    public SafeSingleton getInstance() {
        return SafeSingletonHolder.intance;
    }
}

这样,即使多个线程去调用getInstance()方法,但是在初始化类SafeSingletonHolder时,都能同步进行,因此可以保证线程安全性。


小结

综上所述,单例模式本身很简单,但是如果要是线程安全,还是得有很多考虑。

通过比较发现,基于类初始化的方法比较简单,代码简洁,可以常用。

但是基于volatile的双重检查锁定方法,在延迟初始化非静态实例字段时,唯一可用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值