欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > IT业 > 浅谈单例模式

浅谈单例模式

2025/5/15 17:01:11 来源:https://blog.csdn.net/itigoitie/article/details/144369217  浏览:    关键词:浅谈单例模式

浅谈单例模式

  • 概念
  • 实现方式
    • 1. 饿汉式
    • 2. 懒汉式
    • 3. 内置锁
    • 4. 双重检查锁定
    • 5. 双重检查锁定+volatile
    • 6. 静态内部类(线程安全且推荐)
  • 应用场景
  • 优缺点
    • 优点
    • 缺点
  • 结论

单例模式是一种常用的软件设计模式,其核心目的是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在需要控制资源访问、节省系统资源、管理共享资源以及需要统一调度操作等场景下非常有用。

概念

单例模式属于创建型设计模式,它的核心特点包括:

  • 唯一性:确保一个类只有一个实例。
  • 全局访问点:提供一个全局访问点来获取这个唯一的实例。
  • 延迟初始化:实例在第一次被需要时才创建。

实现方式

单例模式有多种实现方式,包括饿汉式、懒汉式、内置锁、双重检查锁定、双重检查锁定+volatile、静态内部类等。

1. 饿汉式

public class Singleton {private static final 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;}
}

以上参考实现在单线程场景中是合理的、安全的。

在第一次被调用时,getInstance()方法会新建一个Singleton实例,但之后访问时返回的是第一次新建的Singleton实例。

多线程并发访问getInstance()方法时,问题就出来了:不同的线程有可能同时进入if (instance == null)处的条件判断,多次执行代码instance = new Singleton();,从而新建多个Singleton对象。

3. 内置锁

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

getInstance()方法加synchronized关键字之后,可以保证在并发执行时不出错。

问题是:每次执行getInstance()方法都要用到同步,在争用激烈的场景下,内置锁会升级为重量级锁,开销大、性能差,所以不推荐高并发线程使用这种方式的单例模式。

4. 双重检查锁定

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

双重检查锁单例模式主要包括以下三步:

  1. 检查单例对象是否被初始化,如果已被初始化,就立即返回单例对象。这是第一次检查,此次检查不需要使用锁进行线程同步,用于提高获取单例对象的性能。
  2. 如果单例没有被初始化,就试图进入临界区进行初始化操作,此时才去获取锁。
  3. 进入临界区之后,再次检查单例对象是否已经被初始化,如果还没被初始化,就初始化一个实例。这是第二次检查,此次检查在临界区内进行。

为什么在临界区内还需要执行一次检查呢?

答案是:在多个线程竞争的场景下,可能同时不止一个线程通过了第一次检查,此时第一个通过的线程将首先进入临界区,而其他通过的线程将被阻塞,在第一个线程实例化单例对象释放锁之后,其他线程可能获取到锁进入临界区,实际上单例已经被初始化了,所以哪怕进入了临界区,其他线程并没有办法通过第二次检查的条件判断,无法执行重复的初始化。

双重检查不仅避免了单例对象在多线程场景中的反复初始化,而且除了初始化的时候需要现加锁外,后续的所有调用都不需要加锁而直接返回单例,从而提升了获取单例时的性能。

5. 双重检查锁定+volatile

表面上,使用双重检查锁机制的单例模式一切看上去都很完美,其实并不是这样的。

那么问题出现在哪里呢?下面这行代码实际大有玄机:

 //初始化单例instance = new Singleton();

这行初始化单例代码转换成汇编指令后,大致会细分成三个:

  1. 分配一块内存M;
  2. 在内存M上初始化Singleton对象;
  3. M的地址赋值给instance变量;

编译器、CPU都可能对没有内存屏障、数据依赖关系的操作进行重排序,上述的三个指令优化后可能就变成了这样:

  1. 分配一块内存M;
  2. 将M的地址赋值给instance变量;
  3. 在内存M上初始化Singleton对象;

指令重排之后,获取单例可能导致问题的发生,这里假设两个线程以下面的次序执行:

  1. 线程A先执行getInstance()方法,当执行到分配一块内存并将地址赋值给M后,恰好发生了线程切换。此时,线程A还没来得及将M指向的内存初始化。
  2. 线程B刚进入getInstance()方法,判断if语句instance是否为空,此时的instance不为空,线程B直接获取到了未初始化的instance变量。

由于线程B得到的是一个未初始化完全的对象,因此访问instance成员变量的时候可能发生异常。

如何确保线程B获取的是一个完成初始化的单例呢?可以通过volatile禁止指令重排。

双重检查锁+volatile相结合的单例模式实现大致的代码如下:

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;}
}

6. 静态内部类(线程安全且推荐)

public class Singleton {private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}private Singleton() {}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}

使用静态内部类实现单例模式是一种简洁且线程安全的方法,这种方式利用了Java的类加载机制来保证初始化实例时只有一个线程。

这里的关键是:

  1. 静态内部类SingletonHolder是一个私有的静态内部类,它包含一个名为INSTANCE的静态成员变量,该变量持有Singleton类的唯一实例。
  2. 延迟加载:由于SingletonHolder类是私有的,并且没有被直接引用,JVM不会立即加载这个类。只有当getInstance()方法第一次被调用时,SingletonHolder类才会被加载,并且INSTANCE实例才会被创建。这是通过Java的类加载机制实现的延迟加载(Lazy Initialization)。
  3. 线程安全:由于JVM在加载SingletonHolder类时只会创建一个INSTANCE实例,并且这个过程是线程安全的,因此不需要额外的同步措施。
  4. 单例实例的可见性INSTANCE实例在SingletonHolder类中被声明为private static final,这意味着它是一个常量,并且对所有线程都是可见的。

使用静态内部类实现单例模式的优点是简洁、线程安全,并且能够实现延迟加载。这种方式避免了复杂的加锁操作,同时确保了单例实例的唯一性和全局访问性。

应用场景

  1. 配置管理器:系统中的配置信息通常只需要一个配置管理器。
  2. 连接池:数据库连接池确保系统中只有一个连接池实例。
  3. 日志记录器:全局的日志记录器实例。
  4. 线程池:全局的线程池实例。

优缺点

优点

  • 控制资源消耗:由于单例模式限制了实例的数量,它可以减少系统资源的消耗。
  • 统一访问点:提供了一个统一的访问点,便于管理和使用。

缺点

  • 全局状态:单例模式引入了全局状态,可能会导致代码难以测试和维护。
  • 扩展困难:单例模式的扩展不如原型模式灵活。

结论

单例模式是一种简单而强大的设计模式,适用于需要严格控制实例数量的场景。在使用时,需要根据具体的应用需求和环境选择合适的实现方式,并考虑到线程安全和系统性能。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com