设计模式实记——单例模式

确保唯一实例的设计与应用

在软件开发领域,设计模式是解决常见问题的重要经验法则。单例模式是其中之一,它在确保一个类只有一个实例的同时,提供全局访问点。本文将深入探讨七种不同的单例模式实现方案,并以开源框架Spring和个人开源框架Remote(地址: https://gitee.com/asialjim/remote) 为例,阐述单例模式在实际工程应用中的价值。

确保唯一实例的设计与应用

在软件开发领域,设计模式是解决常见问题的重要经验法则。单例模式是其中之一,它在确保一个类只有一个实例的同时,提供全局访问点。本文将深入探讨七种不同的单例模式实现方案,并以开源框架Spring和个人开源框架Remote(地址: https://gitee.com/asialjim/remote) 为例,阐述单例模式在实际工程应用中的价值。


public class EagerSingleton {
    // 类加载d诶时候即实例化
    private static final EagerSingleton instance = new EagerSingleton();
    
    private EagerSingleton() {
        // 私有构造函数
    }
    
    public static EagerSingleton getInstance() {
        return instance;
    }
}

 

2. 懒汉式单例模式

懒汉式单例模式在首次访问实例时才进行实例化,避免了资源浪费。然而,在多线程环境下需要考虑线程安全问题。在Spring中,我们可以使用@Lazy注解实现懒加载。

简单的懒汉式单例模式具有非常严重的新能问题,其保证单例的方案是在获取实例的方法上加锁, 这导致每次调用该方法时都会检查锁占有情况,导致性能极致的底下。


public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {
        // 私有构造函数
    }
    // 当需要的时候才实例化
    // 当前锁级别为方法级别,也就是不管那个线程进来都要检查锁,不论实例是否已经存在
    // 这种模式下,性能较为底下
    public static synchronized LazySingleton getInstance() {
        // 如果调用方法的时候实例不存在才实例化
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

3. 双重检查锁定单例模式

双重检查锁定单例模式是一种在多线程环境下实现延迟加载且线程安全的单例模式。然而,该模式在实现上有一些细微的陷阱,需要小心处理。以下是其优缺点以及为什么需要使用 volatile 关键字的解释:

优点:

  1. 延迟加载: 双重检查锁定单例模式支持延迟加载,即只有在首次需要使用单例实例时才会进行初始化,节省了资源。

  2. 线程安全: 当实现正确时,双重检查锁定单例模式可以在多线程环境下保证线程安全,避免了多线程竞争创建多个实例的问题。

缺点:

  1. 复杂性: 实现双重检查锁定单例模式需要处理一些细节,以确保线程安全。因此,代码相对复杂,容易出错,尤其是在不正确使用的情况下可能会导致线程安全问题。

  2. 可读性降低: 由于需要考虑线程安全和细节,代码的可读性可能会降低,使代码难以维护和理解。

volatile 关键字的作用:

在双重检查锁定单例模式中,volatile 关键字主要用于确保线程间的可见性和禁止指令重排。具体来说,当一个线程访问被 volatile 修饰的变量时,它会从主内存中读取最新的值,而不是使用本地缓存。这样可以确保不同线程之间对该变量的读取操作是同步的。

在双重检查锁定单例模式中,volatile 关键字用于修饰单例实例的引用,例如:


public class DoubleCheckedSingleton {
    private static volatile DoubleCheckedSingleton instance;
    
    private DoubleCheckedSingleton() {
        // 私有构造函数
    }
    
    public static DoubleCheckedSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedSingleton();
                }
            }
        }
        return instance;
    }
}

如果不使用 volatile 关键字,由于指令重排的存在,可能会出现一个线程在创建实例时,另一个线程可能会看到未完全初始化的实例,从而导致单例模式的破坏。

总之,双重检查锁定单例模式在正确使用的情况下能够在多线程环境下实现线程安全的延迟加载。然而,其实现较为复杂,而且需要正确使用 volatile 关键字来确保线程间的可见性和避免指令重排问题。

4. 静态内部类单例模式

静态内部类单例模式是一种常见的单例模式实现方式,它的优缺点如下:

优点:

  1. 延迟加载: 静态内部类单例模式实现了延迟加载(懒加载),即只有在首次访问该单例实例时,才会加载内部类。这可以节省内存和资源,在某些场景下有利于性能优化。

  2. 线程安全: 静态内部类的加载是由 JVM 控制的,JVM 保证了类加载的线程安全性。这使得静态内部类单例模式在多线程环境下是线程安全的,无需使用额外的同步措施。

  3. 无锁化: 由于静态内部类的加载由 JVM 自动处理,不需要使用同步锁或双重检查锁等机制来保证线程安全,从而避免了潜在的性能问题。

  4. 避免反序列化问题: 在静态内部类单例模式中,实例的创建是通过类加载器来完成的,不会受到反序列化的影响,因此不会出现通过反序列化破坏单例的问题。

缺点:

  1. 不支持传参: 静态内部类单例模式的实例创建由 JVM 自动完成,无法在创建实例时传递参数。如果需要在创建实例时传递一些参数,就无法使用这种方式实现单例。

  2. 可读性相对较差: 静态内部类单例模式的实现相对于其他单例模式,可能需要一些理解静态内部类和类加载机制的知识,从而可能降低代码的可读性。

总体来说,静态内部类单例模式是一种简洁、线程安全且具有延迟加载特性的单例实现方式。它适用于大多数场景,并且避免了许多传统单例模式中可能出现的问题,如线程安全性和反序列化问题。


public class StaticInnerSingleton {
    private StaticInnerSingleton() {
        // 私有构造函数
    }
    
    private static class SingletonHolder {
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    }
    
    public static StaticInnerSingleton getInstance() {
        return SingletonHolder.instance;
    }
}

5. 枚举单例模式

枚举单例模式是一种基于枚举类型的实现单例模式的方式,它在Java中是线程安全的且不容易被破坏。以下是枚举单例模式的优缺点:

优点:

  1. 线程安全: 枚举在Java中的创建是线程安全的,因此枚举单例模式天生就是线程安全的,无需担心多线程竞争的问题。

  2. 防止反序列化攻击: 枚举单例模式可以避免通过反序列化来破坏单例模式,因为枚举的实例在序列化和反序列化过程中会被自动保护。

  3. 实现简单: 枚举单例模式非常简单,不需要额外的同步措施、双重检查锁定等,同时也避免了线程安全问题。

  4. 唯一实例: 枚举单例模式天生就是单例的,无需担心反射等方式创建多个实例。

  5. 序列化支持: 枚举单例模式可以很容易地实现序列化和反序列化,而且不会导致新的实例创建。

缺点:

  1. 不支持懒加载: 枚举单例模式无法实现懒加载,即在首次访问之前就已经创建了实例。这可能会在某些情况下造成不必要的资源浪费。

  2. 不支持继承: 由于枚举类型本身不允许继承,因此无法通过继承枚举来扩展单例类。

  3. 可读性较差: 枚举单例模式相对于其他实现方式来说,可能会在代码可读性方面稍显不足,特别是对于不熟悉枚举的开发者来说。

总体来说,枚举单例模式是一种非常安全、简单且易于实现的单例模式。它在大多数情况下都是一个不错的选择,特别是在需要线程安全且不容易被破坏的情况下。然而,如果需要实现懒加载或继承等特性,枚举单例模式可能不是最优选择。


public enum EnumSingleton {
    INSTANCE;
    
    // 添加其他成员和方法
}

6. 容器式单例模式

容器式单例模式通过容器管理多个单例实例,实现按需获取。在Spring中,IoC容器通过单例作用域管理Bean的生命周期,确保单例的唯一性和合适的作用域。严格以JVM作用域范围来讲,容器式单例模式并非是单例模式,只不过在同一个容器作用域下面,只能找到一个实例。在Spring中,IoC容器创建的SpringBean 默认就是单例模式。


@Component
@Scope("singleton")
public class ContainerSingleton {
    // Bean定义和成员
}

7. ThreadLocal单例模式

ThreadLocal单例模式将每个线程与一个单例实例关联,适用于多线程环境。与容器式单例模式一样,ThreadLocal单例模式其作用域仅限定于在一个线程内,保证一个实例。并非严格JVM意义上的单例模式。


public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            ThreadLocal.withInitial(ThreadLocalSingleton::new);
    
    private ThreadLocalSingleton() {
        // 私有构造函数
    }
    
    public static ThreadLocalSingleton getInstance() {
        return threadLocalInstance.get();
    }
}

单例模式在Spring中的应用

开源框架Spring广泛地应用了单例模式。在Spring的IoC容器中,通过默认的单例作用域,Bean的实例在整个应用程序中是唯一的。使用@Component@Service@Repository等注解可以方便地创建单例Bean。同时,Spring的懒加载特性也使得我们可以将单例的创建推迟到首次使用时,以优化性能。

单例模式的实际应用案例

单例模式在实际应用中有着广泛的应用场景。在开发中,我们经常需要确保某些资源、配置或状态是唯一的,并且可以在全局范围内共享。无论是数据库连接池、全局配置、日志管理还是线程池,单例模式都能够提供一种优雅且可靠的解决方案。通过选择合适的单例模式实现,我们可以在保障线程安全的同时,优化性能和资源利用。

在我个人开发的开源框架 Remote(https://gitee.com/asialjim/remote) 当中也用到了单例模式。在该实际开发中,这一个类当中用到了饿汉式,懒汉式,双重检查式,容器式 多种单例模式思想,来完成Remote框架中所需要的功能。代码如下:


@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class RemoteLifeCycleHandlerFactory {
    private static final Logger log = LoggerFactory.getLogger(RemoteLifeCycleHandlerFactory.class);
    // RemoteLifeCycleHandlerFactory 采用饿汉式的单例模式,类加载的时候就实例化
    public static final RemoteLifeCycleHandlerFactory FACTORY = new RemoteLifeCycleHandlerFactory();
    // 饿汉式地提供一个实例容器用于保存需要的实例
    private static final Map<Class<? extends RemoteLifeCycle.LifeCycleHandler<?>>, RemoteLifeCycle.LifeCycleHandler<?>> INSTANCE_CACHE = new ConcurrentHashMap<>();
  
    // 根据类型获取单例实例
    public RemoteLifeCycle.LifeCycleHandler<?> singletonHandler(Class<? extends RemoteLifeCycle.LifeCycleHandler<?>> clazz){
        RemoteLifeCycle.LifeCycleHandler<?> handler = INSTANCE_CACHE.get(clazz);
        // 如果存在实例则直接返回
        if (Objects.nonNull(handler))
            return handler;
        // 锁定资源
        synchronized (INSTANCE_CACHE){
            handler = INSTANCE_CACHE.get(clazz);
            // 双重检查,是否存在实例, 如果存在则直接返回
            if (Objects.nonNull(handler))
                return handler;
            // 创建实例
            Constructor<?> constructor = Arrays.stream(clazz.getConstructors()).filter(item -> item.getParameterCount() == 0).findFirst().orElseThrow(() -> new RuntimeException("RemoteLifeCycleHandler [" + clazz.getName() + "] can not found default constructor..."));
            try {
                if (Modifier.PUBLIC != constructor.getModifiers())
                    throw new IllegalArgumentException("Default Constructor of Class<" + clazz.getName() + "> must be public");

                handler = (RemoteLifeCycle.LifeCycleHandler<?>) constructor.newInstance();
            } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
                // 添加错误日志
                log.warn("Cannot construct instance for class: {}, Exception: {}", clazz.getName(),e.getMessage(),e);
            }
            // 将实例放入缓存,并返回实例
            INSTANCE_CACHE.put(clazz, handler);
            return handler;
        }
    }
}

总结与展望

本文深入探讨了七种Java单例模式的实现方案,并以Spring框架和Remote开源框架为例,展示了单例模式在实际工程应用中的重要性和应用场景。不同的实现方式适用于不同的需求,我们应根据实际情况选择最合适的方式。在下一篇文章中,我们将继续探讨更多设计模式的应用案例。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值