单例模式:确保一个类只有一个实例

目录

引言

[1. 单例模式的核心思想](#1. 单例模式的核心思想)

[2. 单例模式的实现方式](#2. 单例模式的实现方式)

[2.1 饿汉式单例](#2.1 饿汉式单例)

[2.2 懒汉式单例](#2.2 懒汉式单例)

[2.3 线程安全的懒汉式单例](#2.3 线程安全的懒汉式单例)

[2.4 双重检查锁定(Double-Checked Locking)](#2.4 双重检查锁定(Double-Checked Locking))

[2.5 静态内部类实现单例](#2.5 静态内部类实现单例)

[2.6 枚举实现单例](#2.6 枚举实现单例)

[3. 单例模式的使用场景](#3. 单例模式的使用场景)

[4. 单例模式的优缺点](#4. 单例模式的优缺点)

优点:

缺点:

[5. 总结](#5. 总结)


引言

单例模式(Singleton Pattern)是设计模式中最简单且最常用的创建型模式之一。它的核心思想是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式在许多场景中非常有用,例如配置管理、线程池、数据库连接池等。


1. 单例模式的核心思想

单例模式的核心思想是:

  1. 私有化构造函数 :防止外部通过 new 关键字创建实例。

  2. 提供一个静态方法:用于获取类的唯一实例。

  3. 确保唯一性:在整个应用程序生命周期中,类的实例只有一个。


2. 单例模式的实现方式

单例模式有多种实现方式,每种方式都有其优缺点。以下是几种常见的实现方式:

2.1 饿汉式单例

饿汉式单例在类加载时就创建实例,因此是线程安全的。

java 复制代码
public class Singleton {
    // 在类加载时创建实例
    private static final Singleton INSTANCE = new Singleton();

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

    // 提供全局访问点
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

饿汉式是最简单的单例模式的写法,保证了线程的安全,在很长的时间里,我都是饿汉模式来完成单例 的,因为够简单,后来才知道饿汉式会有一点小问题,看下面的代码:

java 复制代码
 public class Hungry {
    private byte[] data1 = new byte[1024];
    private byte[] data2 = new byte[1024];
    private byte[] data3 = new byte[1024];
    private byte[] data4 = new byte[1024];
    
    private Hungry() {
    }
    private final static Hungry hungry = new Hungry();
    public static Hungry getInstance() {
        return hungry;
    }
 }

在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如 果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?我希望的是 只有用 到了 getInstance方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了 第二种单例模 式:懒汉式。

优点

  • 实现简单,线程安全。

缺点

  • 如果实例未被使用,会造成资源浪费。

2.2 懒汉式单例

懒汉式单例在第一次调用 getInstance() 时才创建实例。

java 复制代码
public class LazyMan {
    private LazyMan() {
        System.out.println(Thread.currentThread().getName()+"Start");
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
    // 测试并发环境,发现单例失效
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
                  }).start();
        }
    }
 }

缺点

  • 线程不安全,多个线程可能同时进入 if (instance == null) 条件,导致创建多个实例。

2.3 线程安全的懒汉式单例

通过在 getInstance() 方法上加锁,可以解决懒汉式单例的线程安全问题。

java 复制代码
public class LazyMan {
    private LazyMan() {
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
 }

保证了线程的安全性,又符合了懒加载,只有在用到的时候,才会去初始化,调用 效率也比较高,但是这种写法在极端情况还是可能会有一定的问题。因为 :

lazyMan = new LazyMan();

不是原子性操作,至少会经过三个步骤:

  1. 分配对象内存空间

  2. 执行构造方法初始化对象

  3. 设置instance指向刚分配的内存地址,此时instance !=null;

由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第 二步),此时线程B又进来了,发现lazyMan已经不为空了,直接返回了lazyMan,并且后面使用了返回 的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错 误,所以就有了下面一种单例模式。


2.4 双重检查锁定(Double-Checked Locking)

双重检查锁定是一种优化后的线程安全懒汉式单例实现方式。

这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排:

java 复制代码
 public class LazyMan {
    private LazyMan() {
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

优点

  • 线程安全,且只有在第一次创建实例时加锁,性能较好。

注意

  • 必须使用 volatile 关键字,防止指令重排序导致的问题。

2.5 静态内部类实现单例

静态内部类实现单例是一种优雅且线程安全的方式。

java 复制代码
public class Singleton {
    private Singleton() {}

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

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

优点

  • 线程安全,且只有在调用 getInstance() 时才会加载 SingletonHolder 类,实现懒加载。

2.6 枚举实现单例

枚举实现单例是《Effective Java》推荐的方式,它天然支持线程安全和防止反射攻击。

java 复制代码
public enum Singleton {
    INSTANCE;

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

优点

  • 线程安全,代码简洁,防止反射和序列化破坏单例。

3. 单例模式的使用场景

单例模式适用于以下场景:

  1. 全局配置管理:例如读取配置文件,确保配置信息全局唯一。

  2. 数据库连接池:确保连接池只有一个实例,避免资源浪费。

  3. 日志管理:确保日志记录器全局唯一。

  4. 线程池:确保线程池的唯一性,避免重复创建线程。


4. 单例模式的优缺点

优点:

  • 节省资源:避免重复创建对象,减少内存开销。

  • 全局访问点:方便对唯一实例的管理和访问。

缺点:

  • 扩展性差:单例类通常难以扩展,因为其构造函数是私有的。

  • 违背单一职责原则:单例类既负责创建实例,又负责业务逻辑。

  • 测试困难:单例类的全局状态可能导致测试困难。


5. 总结

单例模式是一种简单但强大的设计模式,适用于需要全局唯一实例的场景。通过不同的实现方式(如饿汉式、懒汉式、双重检查锁定、静态内部类、枚举等),可以满足不同的需求。

在实际开发中,应根据具体场景选择合适的单例实现方式。如果需要懒加载且线程安全,推荐使用静态内部类或枚举实现;如果需要更高的性能,可以考虑双重检查锁定。

相关推荐
倒霉男孩1 天前
单例设计模式---懒汉式--线程安全和不安全、枚举类
安全·单例模式·设计模式
帅的飞起来2 天前
设计模式--单例模式
java·单例模式·设计模式
Amazing_snack2 天前
设计模式--单例模式(Singleton)【C++】
单例模式·设计模式
牵牛老人2 天前
C++设计模式中的单例模式:从原理、应用、实践指南与常见问题和解决方案深度解析
c++·单例模式·设计模式
zzlyyds2 天前
单例模式的五种实现方式
java·开发语言·单例模式·设计模式
techzhi2 天前
设计模式-单例模式
java·单例模式·设计模式
JuicyActiveGilbert3 天前
【C++设计模式】第一篇:单例模式(Singleton)
c++·单例模式·设计模式
小马爱打代码3 天前
设计模式详解(单例模式)
java·单例模式·设计模式
web安全工具库3 天前
深入理解设计模式中的单例模式(Singleton Pattern)
单例模式·设计模式