设计模式之单例模式:说明与应用


前言

Java八股是程序员面试和进阶的必备技能。本专栏旨在每篇文章通过一个八股问题深入钻研,并引申出其相关联的知识点,方便记忆。

本文介绍设计模式的单例模式。将围绕单例模式的概念、实现、应用三个方面展开叙述。


一、单例模式是什么

单例模式(Singleton)是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。


二、单例模式的实现

1.饿汉式

public class Singleton {
    private static Singleton instance=new Singleton();
    private Singleton(){

    }
    public static Singleton getInstance(){
        return instance;
    }
}

2.懒汉式

线程不安全的实现

public class Singleton {
    private static Singleton instance;
    private Singleton(){

    }
    public static Singleton getInstance(){
        if(instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}

线程安全的实现

public class Singleton {
    private static Singleton instance;
    private Singleton(){

    }
    public static synchronized Singleton getInstance(){
        if(instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}

双重检查锁实现

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // 私有化构造函数
    }

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

注意

  1. 构造函数必须是private修饰的,禁止从Singleton类外部调用构造函数创建实例。因为单例类必须自己创建自己的唯一实例,才能确保任何情况下都只能生成一个实例。
  2. 单例类必须对外提供获取其实例的方法。

饿汉式和懒汉式的比较

  1. 实例的初始化时间:饿汉式在初始化类的时候就创建了唯一实例;而懒汉式在外部第一次调用getInstance方法的时候才创建唯一实例
  2. 懒加载的好处是节省资源。但要注意多线程环境下的线程安全问题。

双重检查锁说明

  1. 双重检查锁是对线程安全的懒汉式单例模式的优化。
  2. 线程安全的懒汉式单例模式是在getInstance方法直接加锁,在getInstance方法内部进行一次instance是否为null的检查。双重检查锁是在getInstance方法内部进行一次instance是否为null的检查,为null才加锁,之后再进行一次instance是否为null的检查。
  3. 重点:变量instance必须用volatile修饰。原因:instance = new Singleton();这行代码包含了三步JVM操作。

a. 分配对象的内存空间
b.初始化对象
c.将instance指向刚分配的内存地址

以下并发场景存在问题:线程A进入getInstance方法,执行了步骤1和步骤3,步骤2因为指令重排序的问题还未执行。此时线程B也getInstance,判断instance!=null,直接把当前instance返回了。但由于还未执行步骤2初始化对象,此时instance执行的内存地址是空对象,后续使用会有问题。
如下图,指令重排序导致的问题(绿色表示已执行)
指令重排序导致的问题(绿色表示已执行)

解决:用volatile修饰instance,使得instance = new Singleton();这一步禁用了指令重排序,从而解决了这个问题。

3. 静态内部类

public class Singleton {
    private Singleton() {
        // 私有化构造函数
    }

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

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

静态内部类说明

  1. 静态内部类单例模式是利用java类的加载机制来保证线程安全的。
  2. 当Singleton类被加载时,内部类SingletonHolder不会被加载。只有当调用getInstance方法时,SingletonHolder类才被加载,此时INSTANCE实例也被创建。
  3. 这种方式既能保证线程安全,又能避免使用synchronized关键字,减少加锁的开销,性能较好,是目前公认的最优秀的单例模式的实现方式之一。

4. 枚举

public enum EnumSingleton {
    INSTANCE;
}

枚举说明

  1. 枚举单例模式是使用 Java 枚举特性实现的单例模式。通过私有构造函数和枚举的特性,可以确保只有一个实例被创建。
  2. 这种实现方式非常简单,并且天生就是线程安全的。此外,它还能防止反序列化破坏单例,以及防止通过反射的方式创建新实例。因此,它被认为是最安全和最简单的单例模式实现方式。

三、单例模式的应用

适用场景

  1. 某些类只应该有一个实例,比如配置类、日志类等
  2. 当实例化需要消耗大量资源时,如数据库
  3. 当多个实例会导致问题时,如共享访问修改同一个资源

举例

1. 单例实现DataSource

@Bean
public DataSource dataSource() {
    // 初始化连接池(耗时操作)
    HikariDataSource ds = new HikariDataSource();
    ds.setJdbcUrl("jdbc:mysql://localhost/db");
    return ds; // 单例复用连接池
}

2. 集成腾讯云对象存储时使用单例模式初始化客户端

	@Bean
    public COSClient cosClient() {
        // 初始化用户身份信息(secretId, secretKey)。
        COSCredentials cosCredentials = new BasicCOSCredentials(ServerConfig.cosSecretId, ServerConfig.cosSecretKey);
        ClientConfig clientConfig = new ClientConfig(new Region(ServerConfig.cosBucketRegion));
        clientConfig.setHttpProtocol(HttpProtocol.https);
        return new COSClient(cosCredentials, clientConfig);
    }

@Bean注解不指定其作用域时默认是单例的,相当于@Scope(“singleton”)。

3. Spring用到单例模式的地方

  1. Bean对象的默认作用域为单例(scope=“singleton”):在Spring中,Bean对象的作用域可以配置为单例模式。当一个Bean配置为单例模式时,Spring容器在启动时会创建该Bean的实例,并在容器的整个生命周期中都使用同一个实例。
  2. Spring容器本身是单例的:在Spring中,容器是一个单例对象,只会有一个ApplicationContext容器实例。这是为了提高应用程序的性能和效率,避免重复创建和销毁容器实例。
  3. Spring AOP中的切面类也是单例的:在Spring的AOP中,切面(Aspect)是一个跨越多个对象的通用功能模块,如日志记录、事务管理等。为了保证切面类的效率和性能,Spring默认将切面类配置为单例模式。
  4. Spring中的代理对象也是单例的:在Spring中,通过代理模式来实现增强目标对象的功能。默认情况下,Spring使用JDK动态代理来创建代理对象,并将代理对象配置为单例模式。
  5. 单例容器:在Spring中,可以通过SingletonBeanRegistry接口提供的方法来管理单例对象。这个接口定义了一些方法,如registerSingleton()、getSingleton()、containsSingleton()等,用于向容器注册和获取单例对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值