设计模式之单例

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

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

(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() {
        // 初始化
    }
}

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

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

相关推荐
极客先躯1 分钟前
高级java每日一道面试题-2025年3月21日-微服务篇[Nacos篇]-什么是Nacos?
java·开发语言·微服务
工业互联网专业10 分钟前
基于springboot+vue的动漫交流与推荐平台
java·vue.js·spring boot·毕业设计·源码·课程设计·动漫交流与推荐平台
雷渊12 分钟前
深入分析Spring的事务隔离级别及实现原理
java·后端·面试
try again!15 分钟前
rollup.js 和 webpack
开发语言·javascript·webpack
rebel24 分钟前
Java获取excel附件并解析解决方案
java·后端
du fei26 分钟前
C# 窗体应用(.FET Framework) 线程操作方法
开发语言·c#
du fei28 分钟前
C#文件操作
开发语言·c#
并不会35 分钟前
多线程案例-单例模式
java·学习·单例模式·单线程·多线程·重要知识
数据攻城小狮子36 分钟前
Java Spring Boot 与前端结合打造图书管理系统:技术剖析与实现
java·前端·spring boot·后端·maven·intellij-idea
m0_5557629037 分钟前
struct 中在c++ 和c中用法区别
java·c语言·c++