设计模式之单例

单例可以说是设计模式中最简单的一种模式。但任何一种设计模式都是普遍经验的总结,都有值得思考的地方。所以单例也并不简单,下面让我们慢慢了解它。

单例顾名思义这个类只有一个实例。要做到这点,需要做到以下几点:

(1)构造器私有化

(2)实例只在类内部创建,并且只会创建一次

(3)提供静态方法以供外部获取单例对象

单例有很多种实现方式:

先介绍两种最简单的:懒汉式和饿汉式

(一)懒汉式:顾名思义,这个单例很懒,懒者做事就慢。所以单例对象是等要用的时候再进行实例化的。下面是一个简单的例子

public final class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (Objects.isNull(instance)) {
            instance = new Singleton();
        }
        return instance;
    }
}

(二)饿汉式:我们经常听到老一辈说别人吃饭很急,就会说"像个饿死鬼投胎一样",所以饿汉表现就比懒汉显得急迫,在类加载时就开始实例化。

public final class Singleton {
    private static final Singleton SINGLETON = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return SINGLETON;
    }
}

那么这两种有什么区别呢?

我们应该注意到,单例因为在程序中只有一个实例,所以是线程共享的。那么肯定是在多线程环境下。而懒汉模式是线程不安全的,想象一下多个线程同时在单例还没实例化时进入,那么Objects.isNull(instance)判断为true,从而都会创建实例然后返回,那么有可能返回的是不同的单例。所以懒汉模式几乎不会使用,以此就衍生了线程安全的懒汉模式(这个稍后再介绍)

而饿汉模式虽然因为类加载只会发生一次,而保证了线程安全。但可能会造成内存浪费。比如单例中可能定义了一个常量,而常量在业务场景中可能先于获取单例使用,那么在常量使用时就会进行单例实例化,如下面代码所示:

public final class Singleton {
    private static final Singleton SINGLETON = new Singleton();
    public static final String SINGLETON_CONTENT = "singleton content";

    private Singleton() {
        System.out.println("Singleton intance");
    }

    public static Singleton getSingleton() {
        return SINGLETON;
    }

    public static void main(String[] args) {
        System.out.println(Singleton.SINGLETON_CONTENT);
    }
}

(三)线程安全的懒汉式

想要线程安全,我们首先想到的就是加synchronized锁,保证它的线程安全。于是就有了下面的实现

public final class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    public static synchronized Singleton getInstance() {
        if (Objects.isNull(instance)) {
            instance = new Singleton();
        }
        return instance;
    }
}

但这种方式虽然线程安全了,但它的锁是方法级的。意味着每次获取实例就会加锁,性能却成了问题。于是又再次进化,有了双重校验锁的方式,可以实现高性能且安全的单例模式。

(四)双重校验锁

上面已经说道双重校验锁是进化版的线程安全且高性能的懒汉模式。所以它也是需要用到的时候再实例化,但它是线程安全的。那么它是怎么做到的呢?我们直接上代码:

public final class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        System.out.println("Singleton intance");
    }

    public static Singleton getInstance() {
        // 第一次校验
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次校验
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

如上代码所示,所谓双重检验锁,就是以两次校验+synchronized加锁方式实现的。第一次校验是基于性能考虑,避免每次都进行加锁,因为只有还没实例化时这个短暂且特殊的场景是需要加锁的,大部分时候都是直接返回实例就行。而第二次校验则是保证实例只会创建一次。

另外这里有个关键点,instance单例缓存是加了volatile关键字。这是用到了volatile中的其中一个特性--禁止指令重排,想了解更多关于volatile关键字,可以参看我的另一篇《浅谈volatile

因为实例的创建实际会分为三个步骤

(1)分配内存

(2)初始化对象

(3)将对象指向刚分配的内存空间

如果实例是按这个顺序执行,那么不加volatile,范围的单例也是完成初始化的实力,并无影响。但因为编译器优化,可能会把因为初始化放到最后,提前先将对象指向刚分配的内存空间。那么在单例还没初始化完成时,其他线程进入获取单例时,引用对象就不是空,而提前获取到还没初始化完成的单例引用,就可能会导致后面的业务逻辑出错。

(五)静态内部类

public final class Singleton {
    private Singleton() {
        System.out.println("Singleton intance");
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

}

静态内部类也是懒加载的模式,但它的实现更优雅,是我比较推荐的方式。它利用静态内部类可以访问外部类私有构造器的特性,将单例的实例化延迟到静态内部类类加载时,保证了单例的线程安全。而这个静态内部类只会在获取单例的时候加载,所以也是懒加载。

(六)枚举方式

public enum Singleton {
    INSTANCE;

    Singleton() {
        // 初始化
    }
}

枚举方式实现也很简单,但却最安全,因为它是反序列化。这个是前面的方式做到的。

上面介绍了六种单例实现方式。除了第一和第三种的懒汉式不推荐使用外,其他可以根据实际需要选择。比如通常单例比较简单,那么通常饿汉模式就可以满足。需要延迟加载,那么双重校验锁、静态内部类、枚举类都可以。如果涉及到序列化,那么枚举就是唯一选择了。

相关推荐
m0_748236116 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
倔强的石头10614 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
ProtonBase18 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
Watermelo61719 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v24 分钟前
leetCode43.字符串相乘
java·数据结构·算法
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Evand J2 小时前
LOS/NLOS环境建模与三维TOA定位,MATLAB仿真程序,可自定义锚点数量和轨迹点长度
开发语言·matlab
LucianaiB2 小时前
探索CSDN博客数据:使用Python爬虫技术
开发语言·爬虫·python
Ronin3052 小时前
11.vector的介绍及模拟实现
开发语言·c++
计算机学长大白3 小时前
C中设计不允许继承的类的实现方法是什么?
c语言·开发语言