一、简述
单例模式(Singleton)是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点。单例模式的主要目的是确保在整个应用程序中,某个类只有一个实例存在,从而节省系统资源,避免因为创建过多的对象而导致系统性能下降的问题。同时,单例模式还可以提供一个全局的访问点,方便其他对象获取该实例。
二、单例模式的实现方式
- 饿汉式:在类加载时就创建并初始化单例对象,因此在单例对象使用之前就已经存在。线程安全,但可能会浪费一定的资源。
- 懒汉式:在第一次使用时才创建单例对象,因此可以节省一定的资源。但是需要注意线程安全问题,需要加锁或使用双重检查等方式来保证线程安全。
- Holder 模式:在静态内部类中持有单例对象,在第一次获取单例对象时才创建。线程安全,且不会浪费资源。
- 枚举模式:使用枚举类型来实现单例模式,枚举类型的每个枚举值都是单例对象。线程安全,且可以防止反射和序列化攻击。
三、饿汉式
饿汉式是一种单例模式实现方式,它在类加载时就创建并初始化单例对象,因此在单例对象使用之前就已经存在。
饿汉式单例模式的优点:实现简单,线程安全,因为在类加载时就已经创建了单例对象,不会出现多线程并发访问的问题。
饿汉式单例模式的缺点:可能会浪费一定的资源,因为单例对象在使用之前就已经创建了,如果该单例对象比较大,可能会导致初始化时的大量内存占用。
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
三、懒汉式
懒汉式是一种单例模式实现方式,它在第一次需要使用单例对象时才创建并初始化该对象。
懒汉式单例模式的优点:可以节省一定的资源,因为单例对象只有在需要时才会被创建。
懒汉式单例模式的缺点:需要注意线程安全问题,因为如果多个线程同时调用单例对象的创建方法,可能会导致创建多个单例对象的问题,需要加锁或使用双重检查等方式来保证线程安全。
3.1 懒汉式 1.0
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
懒汉式 1.0 虽然实现了单例模式,但是未解决线程安全的问题。虽然未解决线程安全问题,但是并非表示不可以使用,因为在单线程的环境中懒汉式 1.0 既可以达到节省资源又能保证线程安全。
3.2 懒汉式 2.0
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
// 新增了一个 synchronized 关键字解决线程同步问题
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
懒汉式 2.0 通过 synchronized 关键字保证了多线程环境中的安全,但是并不能保证多线程环境中的高性能。在性能要求不高的多线程场景中可以使用,如若是性能敏感的多线程场景不建议使用。
为什么懒汉式 2.0 会影响性能?
synchronized 关键字会导致线程同步(即多个线程同时获取还未创建的单例对象时,因为 synchronized 关键字会导致只有一个线程能够获取到锁而其他线程陷入排队等待)
3.3 懒汉式 3.0
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
// 新增了一层检查判断
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
懒汉式 3.0 在获取锁之前进行了一次检查,获取锁之后又进行了一次检查,这就是所谓的双重检查锁(Double Check Lock,简称 DCL)。
- 第一次检查是为了避免产生同步开销。例如:若单例对象已经存在了,那么通过第一次检查就不会进入同步块,也就不会进行加锁、解锁的流程使得性能提升。
- 第二次检查是为了保证单例线程安全。例如:若单例对象还未创建,多个线程同时通过了第一次检查,第一个线程进入同步代码块完成了单例对象的创建。随后的线程进入同步代码块,通过第二次检查时发现单例对象已存在便会放弃创建单例对象从而保证了线程安全。
似乎,DCL 已经解决了 synchronized 所带来的性能问题。但是很不幸,事实并非如此。由于指令重排序,可能会出现获取到半个对象的情况。基于上述情况,懒汉式 3.0 并不完善,最好不要用于生产实践。
3.4 懒汉式 4.0
public class Singleton {
// 使用 volatile 解决指令重排序问题
private static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
懒汉式 4.0 在 懒汉式 3.0 的基础上新增了 volatile 关键字解决了指令重排序的问题。在多线程环境中且对性能要求较高,可以采用此实现方式。
四、Holder 模式
从上文中可以看出:饿汉式拥有创建简单且能保证线程安全,懒汉式有避免资源浪费的优点。若想结合这两个优点那么 Holder 模式便能实现。
public class Singleton {
// 内部类
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
private SingletonHolder() {
}
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
Holder 机制的核心是静态内部类,通过静态内部类来持有单例对象,实现延迟加载和线程安全。具体来说,当第一次调用 getInstance() 方法时,会通过静态内部类 SingletonHolder 来创建单例对象。由于静态内部类只会被加载一次,因此可以保证单例对象只会被创建一次,而且只有在需要时才会被创建。
五、枚举模式
public enum Singleton {
SINGLETON;
}
上面的代码便实现了一个单例。是不是太简单了?看看这几行代码的本质。
// 这是反编译之后得到的,可以看出其本质就是饿汉式
public class Singleton extends Enum<Singleton> {
public static final Singleton SINGLETON = new Singleton();
}
枚举模式实现的单例与饿汉式实现的单例其本质是一样的,差异是枚举采用的是 public static 成员变量,而饿汉式采用的是 private static 成员变量。
六、FAQ
- 指令重排序是指编译器和处理器为了提高程序性能,对指令序列进行优化,改变指令的执行顺序,但是不会影响程序的最终结果的技术。在计算机系统中,为了提高程序的执行效率,编译器和处理器会对指令进行重排序,以使程序能够更快地执行。但是,指令重排序可能会导致程序出现意想不到的结果,因为程序的执行顺序不再是程序员所期望的顺序。为了避免指令重排序带来的问题,Java 提供了 volatile 关键字和 synchronized 关键字等同步机制,可以保证多线程之间的可见性和有序性,从而避免指令重排序带来的问题。
本文详细介绍了Java中的单例模式,包括饿汉式、懒汉式(1.0、2.0、3.0、4.0)、Holder模式以及枚举模式的实现方式,讨论了它们的优缺点,特别关注了线程安全和资源管理。还提到了指令重排序在多线程中的影响以及解决策略。
2329

被折叠的 条评论
为什么被折叠?



