从JDK中学习设计模式——单例模式

818 阅读5分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

概述

单例模式(Singleton Pattern)是一种对象创建型模式,它确保了某个类只有一个实例,并且自行实例化来供外部使用。为了防止在外部对其实例化,会将其构造函数设计为私有。单例模式分为两大类,分别是饿汉式单例懒汉式单例

单例模式UML.png

饿汉式

饿汉式单例类是实现起来最简单的单例类,由于在定义静态变量的时候实例化了单例类,因此在类加载的时候就会创建该单例对象,饿汉式的”饿“体现在这里。其示例如下:

public class EagerSingleton {
    
    private static final EagerSingleton instance = new EagerSingleton();
    
    private EagerSingleton() { } 
 
    public static EagerSingleton getInstance() {
        return instance; 
    }   
}

当 EagerSingleton 类被加载时,静态变量 instance 会被初始化,此时该类的私有构造函数会被调用,从而创建该类的唯一实例。

  • 优点:写法简单;类加载就会实例化,程序性能高。
  • 缺点:无论实例是否被使用都会被创建,因此会导致内存的浪费

懒汉式

懒汉式单例在类加载时并不自行实例化,它在调用 getInstance() 方法时,先判断单例类是否实例化,若没有实例化才被创建。,为了避免多个线程同时调用 getInstance() 方法,使用了 synchronized 关键字,从而保证该单例类的实例唯一

实例在被需要时才被创建,这种技术称为延迟加载(Lazy Load),也是因为这种“火烧眉毛”的方式,所以被称为懒汉式。

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

懒汉式的改进

懒汉式单例为了保证单例类在多线程中的唯一性,在 getInstance() 方法前面增加了 synchronized 关键字。虽然这种方式解决了线程安全问题,但是每次调用 getInstance() 时都需要进行线程锁定判断,在高并发访问环境中,会导致系统性能大大降低。为了解决这个问题,需要对懒汉式进行改进,我们无须对整个 getInstance() 方法进行锁定,只需对创建实例的代码进行锁定即可。 getInstance() 方法改进如下:

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

懒汉式进一步的改进

使用以上代码来实现单例,还是会存在单例对象的不唯一。假如在某个瞬间线程 A 和线程 B 同时调用 getInstance(),此时 instance 对象为 null,都能通过 instance == null 的判断。由于对单例类的加锁,线程 A 进入 synchronized 锁定的代码中执行实例的创建,线程 B 处于排队等待状态。但当 A 执行完毕时,线程 B 并不知道实例已经创建,将继续创建新的实例,因此会创建多个单例对象,这就违背了单例模式的设计思想,因此需要进行进一步改进,在 synchronized 代码中再进行一次 instance == null 判断,这种方式称为双重检查锁定(DCL,Double-Check Locking)。其代码所示如下:

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

注意:使用双重检查锁定来实现懒汉式单例,需要对静态成员变量 instance 增加 volatile 修饰符,以此来保证多个线程之间的可见性,防止指令重排,确保实例创建的唯一性。由于 volatile 关键字会屏蔽 JVM 所做的一些优化,可能会导致系统运行效率降低。

懒汉式的更好实现

上述代码虽然保证了在多线程环境中单例类的唯一,但是会导致性能的下降。在 Java 中我们可以通过静态内部类来创建单例对象,再将该单例对象通过getInstance() 方法返回给外部使用,这种技术称为IoDH(Initialization Demand Holder),其实现如下:

public class LazySingleton {
    
	private LazySingleton() {}
	
	private static class LazyInstanceClass {
        private final static Singleton instance = new Singleton();
	}
	
	public static LazySingleton getInstance() {
	    return LazyInstanceClass.instance;
	}
}

由于静态单例对象 instance 不是单例类 LazySingleton 的成员变量,因此在单例类加载时,不会被实例化。只有在第一次调用 getInstance() 方法时,才会通过静态内部类 LazyInstanceClass 初始化该类下的静态成员变量 instance,由 JVM 来保证其线程的安全性,确保该成员变量只初始化一次。

懒汉式的再完善

通过 IoDH,既实现了延迟加载,又保证了线程的安全,但仍然可以通过反射攻击或者反序列化攻击打破单例类的唯一性。

  • 反射攻击:
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
  • 反序列化攻击:
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);

枚举可以解决这些问题,最佳的单例实现模式就是枚举模式。利用枚举的特性,可以让 JVM 来保证线程的安全和单一实例的问题。其实现如下:

public enum EnumSingle {

    INSTANCE;
    
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

优点

  1. 内存中始终只有一个实例,节省了内存,避免了对资源的多重占用。
  2. 提升系统性能,当一个对象需要的资源较多时,可以初始化一个实例,然后永久驻留在内容中,如读取配置。

缺点

单例模式一般没有接口,不利于扩展。

应用场景

  1. 在整个项目中需要一个共享访问点或共享数据。

  2. 创建一个对象所消耗的资源过多,如访问IO和数据库等资源。

  3. 需要定义大量的静态常量和静态方法的环境,如工具类。

JDK 中的应用

JDK 中的 java.lang.Runtime 就是单例模式中的饿汉式。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}

参考链接

Java单例模式:为什么我强烈推荐你用枚举来实现单例模式