Singleton Pattern 单例模式

Singleton Pattern 单例模式,作为创建型模式的一种,其保证了类的实例对象只有一个,并对外提供此唯一实例的访问接口。

23中设计模式

在这里插入图片描述

概述

对于单例模式而言,其最核心的目的就是为了保证该类的实例对象是唯一的。为此一方面,需要将该类的构造函数设为private,另一方面,该类需要在内部完成实例的构造并对外提供访问接口。单例模式的好处显而易见,可以避免频繁创建、销毁实例所带来的性能开销;但其缺点也同样明显,此类不仅需要描述业务逻辑,同时还需要构造出该类的唯一对象并对外提供访问接口,其显然违背了单一职责原则

实现

单例模式的思想虽然简单易懂,但实现起来却可谓是花样繁多、妙不可言。这里来介绍几种常见的单例模式的实现

饿汉式

如下实现最为简单,当 SingletonDemo1 类被加载到JVM中,即会完成实例化。即不是所谓的Lazy Load 延迟加载,故通常被称之为 “饿汉式” 单例。饿汉式类加载到内存后,就实例化一个单例,JVM保证线程安全。

其最大的问题就在,可能构造出来的实例对象从头到尾没有被使用过(没有调用过getInstance方法),从而浪费内存。可能有人会对此有些困惑,MessageResource01 类被加载到JVM中了,那肯定是因为调用了getInstance方法啊。难道还有别的原因?肯定有!如果SingletonDemo1类中还有其他静态方法,一旦被调用就会导致MessageResource01类被加载、初始化,此时即完成了实例的构造。

/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化 比如 Class.forName("")
 * (话说你不用的,你装载它干啥)
 */
public class MessageResource01 {

    private static final MessageResource01 INSTANCE = new MessageResource01("我是饿汉式单例模式!");

    private String description;

    /**
     * 私有构造器
     * @param message
     */
    private MessageResource01(String message){
        this.description = message;
    }

    public void getInfo() {
        System.out.println(description);
    }

    /**
     * 提供实例的访问接口
     * @return
     */
    public static MessageResource01 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        MessageResource01 messageResource = MessageResource01.getInstance();
        messageResource.getInfo();
    }
}
测试结果
在这里插入图片描述

懒汉式

前面说到,饿汉式单例会导致内存空间的浪费,那么有没有办法解决这个问题呢?答案是有的,这就是"懒汉式"单例。顾名思义,其实例不是在类加载、初始化时被构建的,而是在真正需要的时候才去创建,如下所示

/**
 * 懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 */
public class MessageResource02 {

    private static MessageResource02 INSTANCE ;

    private String description;

    /**
     * 私有构造器
     *
     * @param message
     */
    private MessageResource02(String message) {
        this.description = message;
    }

    public void getInfo() {
        System.out.println(description);
    }

    /**
     * 提供实例的访问接口
     *
     * @return
     */
    public static MessageResource02 getInstance() {
        if(INSTANCE == null){
            INSTANCE = new MessageResource02("我是线程不安全的懒汉式单例模式!");
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        MessageResource02 messageResource = MessageResource02.getInstance();
        messageResource.getInfo();
    }
测试结果
在这里插入图片描述

"懒汉式"单例虽然实现了Lazy Load延迟加载,但是其存在一个很严重的问题,不是线程安全的。所以如果在多线程环境下,我们需要使用下面线程安全的"懒汉式"单例,其保障线程安全的手段也很简单,直接使用synchronized来修饰getInstance方法。这种办法过于简单粗暴,同时会导致效率十分低下。实例一旦被构造完毕后,由于锁的存在,导致每次只能由一个线程可以获取到实例对象

/**
 * 懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class MessageResource03 {

    private static MessageResource03 INSTANCE ;

    private String description;

    /**
     * 私有构造器
     *
     * @param message
     */
    private MessageResource03(String message) {
        this.description = message;
    }

    public void getInfo() {
        System.out.println(description);
    }

    /**
     * 提供实例的访问接口
     *
     * @return
     */
    public static synchronized MessageResource03 getInstance() {
        if(INSTANCE == null){
            INSTANCE = new MessageResource03("我是带锁的懒汉式单例模式!");
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        MessageResource03 messageResource = MessageResource03.getInstance();
        messageResource.getInfo();
    }
}

基于DCL(Double-Checked Locking)双重检查锁的单例

通过前面我们看到,无论是饿汉式单例还是懒汉式单例,其都有明显的缺点。那么有没有一种完美的单例?既可以实现Lazy Load延迟加载,又可以在保证线程安全的前提下依然具备较高的效率呢。答案是肯定——基于DCL(Double-Checked Locking)双重检查锁的单例。其实现如下,该单例实现中进行了两次检查。第一次检查时如果发现实例已经构造完毕了,则无需加锁直接返回实例对象即可。其保证了实例在构建完成后,其他多个线程可以同时快速获取该实例。第二次检查时则是为了避免重复构造实例,因为在还未构造实例前,可能会有多个线程通过了第一次检查,准备加锁来构造实例。在DCL的单例实现中,尤其需要注意的一点是静态变量instance必须要使用volatile进行修饰。其原因在于volatile禁止了指令的重排序。这里解释一下为什么要加volatile?
在这里插入图片描述

new 一个对象会分为两步,一步初始化,一步赋值(俗称的版初始化状态)。如果不加volatile,那么有可能指令乱序(new对象见上图1,3步骤),这样就有可能在初始化后,直接赋值给INSTANCE,导致下个线程来时,读到不为空的INSTANCE,这样返回的就是初始化值。

/**
 * DCL模式
 *
 * @author Mingchong
 * @date 2022年03月19日 11:42
 */
public class MessageResource04 {
		//此处必须加volatile修饰
    private static volatile MessageResource04 INSTANCE ;

    private String description;

    /**
     * 私有构造器
     *
     * @param message
     */
    private MessageResource04(String message) {
        this.description = message;
    }

    public void getInfo() {
        System.out.println(description);
    }

    /**
     * 提供实例的访问接口
     *
     * @return
     */
    public static MessageResource04 getInstance() {
        if(INSTANCE == null){
            synchronized (MessageResource04.class){
                if(INSTANCE == null){
                    INSTANCE = new MessageResource04("我是DCL双重检查锁单例模式!");
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        MessageResource04 messageResource = MessageResource04.getInstance();
        messageResource.getInfo();
    }
}
测试结果
在这里插入图片描述

基于静态内部类的单例

前面我们说到的第一种单例实现,之所以被称为饿汉式、非延迟加载。加载外部类时不会加载内部类,这样可以实现懒加载。同样地,该方式的单例也是满足线程安全的。

/**
 * 静态内部类方式
 * JVM保证单例
 * 加载外部类时不会加载内部类,这样可以实现懒加载
 *
 */
public class MessageResource05 {

    private String description;

    /**
     * 私有构造器
     *
     * @param message
     */
    private MessageResource05(String message) {
        this.description = message;
    }

    public void getInfo() {
        System.out.println(description);
    }

    /**
     * 提供实例的访问接口
     *
     * @return
     */
    private static class MessageResource05Holder{
        private static final MessageResource05 INSTANCE = new MessageResource05("我是静态内部类的单例模式!");
    }

    public static MessageResource05 getInstance(){
        return MessageResource05Holder.INSTANCE;
    }

    public static void main(String[] args) {
        MessageResource05 messageResource = MessageResource05.getInstance();
        messageResource.getInfo();
    }
}
测试结果
在这里插入图片描述

基于枚举的单例

对于Java的枚举类型而言,其构造器是且只能是private私有的。故其特别适合用于实现单例模式。下面即是一个基于枚举的单例实现,可以看到此种实现非常简洁优雅。当枚举类进行加载、初始化时,即会完成实例的构建,我们通过枚举的特性保证了实例的唯一性,当然其不是Lazy Load延迟加载的。与此同时根据类的加载机制我们可知其也是线程安全的(由JVM保证),不仅可以解决线程同步,还可以防止反序列化。

/**
 * 枚举
 * 不仅可以解决线程同步,还可以防止反序列化。
 *
 */
public enum MessageResource06 {

    /**
     * 单例
     */
    INSTANCE("我是枚举的单例模式");

    private String description;

    /**
     * 枚举的构造器默认访问权限是private, 当然也只能是私有的
     *
     * @param message
     */
    MessageResource06(String message) {
        this.description = message;
    }

    public void getInfo() {
        System.out.println(description);
    }

    public static void main(String[] args) {
        MessageResource06 messageResource = MessageResource06.INSTANCE;
        messageResource.getInfo();
    }
测试结果
在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值