问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

线程安全的单例模式

创作时间:
作者:
@小白创作中心

线程安全的单例模式

引用
CSDN
1.
https://m.blog.csdn.net/Flying_Fish_roe/article/details/143932805

单例模式是Java编程中常用的设计模式之一,它确保一个类只有一个实例,并提供一个全局访问点。在多线程环境下,如何实现线程安全的单例模式是一个关键问题。本文将详细介绍几种常见的线程安全单例模式实现方式及其优缺点。

在 Java 编程中,单例模式(Singleton Pattern)是一种常用的设计模式,它保证在整个应用程序中,某个类只有一个实例,并提供一个全局访问点。单例模式有多种实现方式,而其中一个关键问题是如何实现线程安全的单例模式。在多线程环境下,如果单例模式没有正确实现线程安全,就会导致多个线程同时创建多个实例,从而破坏单例模式的初衷。

一、单例模式的基本概念

单例模式确保一个类在应用程序中只有一个实例,并提供一个全局访问点。典型的单例模式有以下几个特点:

  1. 私有构造方法 :单例类的构造方法是私有的,防止外部类通过 new 关键字创建多个实例。
  2. 静态私有成员变量 :类中包含一个静态私有成员变量,用于保存单例实例。
  3. 公有静态方法 :提供一个静态公有方法来返回类的唯一实例,通常称为 getInstance() 方法。

二、线程安全的单例模式实现方法

1. 饿汉式(Eager Initialization)

饿汉式 单例在类加载时就创建实例,因此它天生是线程安全的,因为在类加载时,JVM 会保证只有一个线程去初始化类。

public class SingletonEager {
    private static final SingletonEager INSTANCE = new SingletonEager();

    // 私有构造方法,防止外部实例化
    private SingletonEager() {}

    // 提供公共访问点
    public static SingletonEager getInstance() {
        return INSTANCE;
    }
}

优点

  • 实现简单,天生线程安全。
  • 类加载时创建实例,避免了多线程同步问题。

缺点

  • 在类加载时就创建实例,如果实例初始化非常耗时或占用大量资源,而应用程序并不一定需要这个实例,可能会造成资源浪费。
2. 懒汉式(Lazy Initialization)

懒汉式 单例只有在第一次需要使用时才会创建实例,这样可以避免资源浪费。但是在多线程环境下,如果没有正确同步,会导致多个线程同时创建多个实例。

线程不安全的懒汉式实现

public class SingletonLazy {
    private static SingletonLazy instance;

    private SingletonLazy() {}

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

这种实现方式在单线程环境下可以正常工作,但在多线程环境下可能会出现问题,比如两个线程同时检查 instance == null,然后同时创建实例。

线程安全的懒汉式实现(同步方法)

public class SingletonLazyThreadSafe {
    private static SingletonLazyThreadSafe instance;

    private SingletonLazyThreadSafe() {}

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

通过在 getInstance() 方法上加 synchronized 关键字,可以确保在多线程环境下只有一个线程能进入创建实例的代码块。

优点

  • 线程安全,且仅在第一次创建实例时进行同步,性能高效。

缺点

  • 每次调用 getInstance() 方法都需要进行同步检查,可能影响性能。
3. 双重检查锁定(Double-Checked Locking)

双重检查锁定 是一种优化的线程安全单例实现方式,通过减少同步开销提高性能。它在第一次检查 instance == null 时不进行同步,只有在第一次检查通过之后再进行同步检查。

public class SingletonDoubleChecked {
    private static volatile SingletonDoubleChecked instance;

    private SingletonDoubleChecked() {}

    public static SingletonDoubleChecked getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (SingletonDoubleChecked.class) {
                if (instance == null) { // 第二次检查
                    instance = new SingletonDoubleChecked();
                }
            }
        }
        return instance;
    }
}

注意instance 变量使用 volatile 修饰,确保在多线程环境下,instance 的变化能够立即被其他线程看到,防止指令重排序带来的问题。

优点

  • 线程安全,且仅在第一次创建实例时进行同步,性能高效。

缺点

  • 实现相对复杂,需要正确处理双重检查逻辑。
4. 静态内部类(Static Inner Class)

静态内部类 实现单例模式是一种非常优雅和高效的方式,利用了类加载的特点来实现线程安全的懒加载。

public class SingletonInnerClass {
    private SingletonInnerClass() {}

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

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

在这种实现方式中,SingletonHolder 是一个静态内部类,它的实例只有在 getInstance() 方法第一次调用时才会被加载。JVM 会确保类的加载过程是线程安全的,因此不需要显式的同步。

优点

  • 延迟加载,线程安全。
  • 实现简单,性能高效,没有同步开销。

缺点

  • 基于类加载的实现方式,可能在某些反射或序列化的场景下需要额外处理。
5. 枚举单例(Enum Singleton)

枚举单例 是实现单例模式的最佳实践之一,它不仅解决了线程安全问题,还防止了反序列化和反射攻击。

public enum SingletonEnum {
    INSTANCE;

    public void doSomething() {
        System.out.println("Do something");
    }
}

Java 枚举类型的特点决定了它天生是单例的。JVM 保证枚举实例的唯一性,防止通过反射或反序列化重新创建实例。

优点

  • 实现简单,线程安全。
  • 防止反序列化和反射攻击。
  • 高效,无同步开销。

缺点

  • 枚举类无法继承其他类,可能不适用于需要继承的场景。

三、单例模式的线程安全问题

在多线程环境下实现单例模式时,需要特别注意以下几个问题:

  1. 指令重排序 :在没有 volatile 修饰符的情况下,JVM 和 CPU 可能会对指令进行重排序,这会导致线程看到未初始化完成的实例。通过使用 volatile 修饰符可以避免这种问题。

  2. 双重检查锁定问题 :在双重检查锁定实现中,需要特别小心编写检查和同步代码,以确保正确的同步。

  3. 延迟加载问题 :某些单例实现(如饿汉式)在类加载时就创建实例,这可能导致不必要的资源占用。通过使用懒汉式或静态内部类的方式,可以有效解决这个问题。

  4. 反序列化破坏单例 :默认情况下,序列化和反序列化操作会创建新的对象实例,这可能破坏单例模式。可以通过实现 readResolve() 方法来防止这种情况。

  5. 反射攻击 :反射可以访问私有构造方法,从而创建多个实例。可以通过在构造方法中添加防御代码来防止反射攻击。

四、单例模式的应用场景

单例模式广泛应用于以下场景:

  1. 全局唯一实例 :需要确保一个类在整个应用程序中只有一个实例,例如日志管理器、线程池、数据库连接池、配置管理器等。

  2. 共享资源 :单例模式用于共享资源的管理,如共享缓存、共享对象池等。

  3. 控制实例化 :在某些情况下,控制实例化过程可以节省资源或提高性能。例如,应用程序中某些资源非常宝贵或昂贵,需要严格控制其实例化数量。

五、总结

线程安全的单例模式是 Java 编程中的一个常见且重要的设计模式。不同的实现方式各有优劣,开发者应根据具体的应用场景和需求选择合适的实现方式。饿汉式实现简单且天生线程安全,但可能导致资源浪费;懒汉式提供了延迟加载,但需要同步处理来保证线程安全;双重检查锁定优化了性能,但实现复杂;静态内部类和枚举单例模式是目前最推荐的实现方式,既简单高效,又避免了同步开销和反射、序列化攻击等问题。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号