一、基本概念
单例设计模式是⼀种确保⼀个类只有⼀个实例,并提供⼀个全局访问点来访问该实例的创建模式。
关键概念:
- 一个私有构造函数:确保只能单例类自己创建实例
- 一个私有静态变量:确保只有一个实例,私有静态变量用于保存该类的唯一实例
- 一个公有静态函数:给使用者提供调用方法
优点:
有些实例全局只需要⼀个,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。
二、实现方法
单例模式有两种类型:
- 懒汉式:在真正需要使用对象时,采取创建该单例类对象
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
1. 懒汉式(线程不安全)
懒汉式创建对象方法在程序使用前会先判断该对象是否已经实例化(判断是否为空),若已实例化直接返回该类对象,否则执行实例化。
class Singleton{
private static Singleton instance;
private Singleton(){} // 构造函数为私有,确保外界不可以使用new创建该类实例
public static Singleton GetInstance(){ // 该方法为本类实例的唯一全局访问点
if (instance == NULL){ // 若实例不存在,则new一个实例,否则返回已有实例
instance = new Singleton();
}
return instance;
}
};
2. 懒汉式(线程安全)
在这里考虑线程安全问题,如果多个线程同时判断instance为空,那么他们都会去实例化一个Singleton对象,就违背了单例模式的原则。为了保证线程安全,考虑加上锁。
public static synchronized Singleton getInstance() {
synchronized(Singleton.class) {
if (instance== NULL) {
instance= new Singleton();
}
}
return instance;
}
3. 双重检查锁
上述代码虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了, 但是锁还在,每次还是只能拿到锁的线程进入该方法使线程阻塞,等待时间过长。
将锁的位置改变,并且多加了⼀个检查。也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而还没有实例化的时候多个线程进去也没有事,因为里面的方法有锁,只会让⼀个线程进⼊最内层方法并实例化实例。如此⼀来,最多也就是第⼀次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。
class Singleton{
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance() {
if (uniqueInstance== NULL) {
synchronized(Singleton.class) { // 加锁,只有一个线程获得该锁并进行初始化
if (uniqueInstance== NULL) {
uniqueInstance= new Singleton();
}
}
}
return uniqueInstance;
}
}
为什么使用volatile关键字修饰uniqueInstance变量?
uniqueInstance= new Singleton();
上述一行代码在执行时分为三步:
- 为uniqueInstance分配空间;
- 初始化uniqueInstance;
- 将uniqueInstance指向分配的内存地址。
采用volatile会禁止JVM的指令重排(指令重排会导致有些协程获取到还没有初始化的实例),保证多线程环境下的安全运行。
4. 饿汉式
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即:在编码时已经指明马上创建该对象,不需要等待调用时再创建。
class Singleton{
private static final Singleton instance= new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
};
5. 静态内部类
当外部类Singleton被加载时,静态内部类SingletonHolder并没有被加载金内存,当调用getInstance()方法时,才会触发SingletonHolder.instance,此时静态内部类才会被加载进内存,并且初始化instance实例。该方法延迟了实例化,节约了资源,且线程安全,性能也提高了。
class Singleton{
private Singleton(){}
// 静态内部类持有实例
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
// 公告静态方法,返回实例
public static Singleton getInstance(){
return SingletonHolder.instance;
}
};
6. 枚举类
默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。
public enum Singleton{
INSTANCE;
// 可以添加其他方法和属性
public void doSomething(){
// 实现...
}
}
总结:
(1)单例模式常见的写法有两种:懒汉式、饿汉式
- 懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题
- 饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
(2)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题
(3)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加volatile关键字防止指令重排序
(4)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。
三、应用场景
单例设计模式适用于以下⼀些场景:
- 资源共享:当多个模块或系统需要共享某⼀资源时,可以使用单例模式确保该资源只被创建⼀次,避免重复创 建和浪费资源。
- 控制资源访问:单例模式可以用于控制对特定资源的访问,例如数据库连接池、线程池等。
- 配置管理器:当整个应用程序需要共享⼀些配置信息时,可以使用单例模式将配置信息存储在单例类中,方便全局访问和管理。
- 日志记录器:单例模式可以用于创建⼀个全局的日志记录器,用于记录系统中的日志信息。
- 线程池:在多线程环境下,使用单例模式管理线程池,确保线程池只被创建⼀次,提高线程池的利⽤率。
- 缓存:单例模式可以⽤于实现缓存系统,确保缓存只有⼀个实例,避免数据不⼀致性和内存浪费。