共计 3510 个字符,预计需要花费 9 分钟才能阅读完成。
单例模式概述
单例模式是一种对象创建模式,用于产生一个类的具体事例。使用单例模式可以确保整个系统中单例类只产生一个实例。有下面两大好处:
- 对于频繁创建的对象,节省初第一次实例化之后的创建时间。
- 由于 new 操作的减少,会降低系统内存的使用频率。减轻 GC 压力,从而缩短 GC 停顿时间
创建方式:
- 单例作为类的私有 private 属性
- 单例类拥有私有 private 构造函数
- 提供获取实例的 public 方法
单例模式的角色:
角色 | 作用 |
---|---|
单例类 | 提供单例的工厂,返回类的单例实例 |
使用者 | 获取并使用单例类 |
类基本结构:
单例模式的实现
1. 饿汉式
public class HungerSingleton {
//1. 饿汉式
// 私有构造器
private HungerSingleton() {System.out.println("create HungerSingleton");
}
// 私有单例属性
private static HungerSingleton instance = new HungerSingleton();
// 获取单例的方法
public static HungerSingleton getInstance() {return instance;}
}
注意:
- 单例修饰符为 static JVM 加载单例类加载时,直接初始化单例。无法延时加载。如果此单例一直未被使用,单 Singleton 因为调用静态方法被初始化则会造成内存的浪费。
- getInstance()使用 static 修饰,不用实例化可以直接使用 Singleton.getInstance()获取单例。
- 由于单例由 JVM 加载类的时候创建,所以不存在 线程安全 问题。
2. 简单懒汉式
public class Singleton {//2.1 简单懒汉式(线程不安全)
// 私有构造器
private Singleton() {System.out.println("create Singleton");
}
// 私有单例属性[初始化为 null]
private static Singleton instance = null;
// 获取单例的方法
public static Singleton getInstance() {if(instance == null) {
// 此处 instance 实例化
// 首次调用单例时会进入 达成延时加载
instance = new Singleton();}
return instance;
}
}
- 由于未使用 synchronized 关键字,所以当线程 1 调用单例工厂方法 Singleton.getInstance() 且 instance 未初始化完成时,线程 2 调用此方法会将 instance 判断为 null,也会将 instance 重新实例化赋值,此时则产生了多个实例!
- 如需线程安全可以直接给 getInstance 方法上加 synchronized 关键字, 如下:
public class Singleton {//2.2 简单懒汉式(线程安全)
// 私有构造器
private Singleton() {System.out.println("create Singleton");
}
// 私有单例属性[初始化为 null]
private static Singleton instance = null;
// 获取单例的方法 将此方法使用 synchronized 关键字同步
public static synchronized Singleton getInstance() {if(instance == null) {
// 此处 instance 实例化
// 首次调用单例时会进入 达成延时加载
instance = new Singleton();}
return instance;
}
}
面临的问题:
- 由于对 getInstance()整个方法加锁,在多线程的环境中性能比较差。
3.DCL 懒汉式(双重检测)
简单懒汉式(线程安全)中,对 getInstance()方法加锁,导致多线程中性能较差,那么是否可以 减小锁的范围 ,使不用每次调用 geInstance() 方法时候都会去竞争锁?
DCL(Double Check Locking)双重检测 就是这样一种实现方式。
传统 DCL:
public class DCLLazySingleton {
//3.DCL
// 私有构造器
private DCLLazySingleton() {System.out.println("create DCLLazySingleton");
}
//step1 私有单例属性[初始化为 null] volatile 保证内存可见性 防止指令重排
private static volatile DCLLazySingleton instance = null;
// 获取单例的方法
public static DCLLazySingleton getInstance() {
// 这里判 null 是为了在 instance 有值时,不进入加锁的代码块,提高代码性能。if(instance == null) {
// 缩小锁范围 由于是静态方法方法调用的时候不依赖于实例化的对象 加锁只能使用类
synchronized (DCLLazySingleton.class) {
// 这里判 null 是为了配合 volatile 解决多线程安全问题
if(instance == null) {instance = new DCLLazySingleton();
}
}
}
return instance;
}
}
注意:
-
传统 DCL(未使用 volatile 或在 JDK1.8 之前版本)面临的问题:
- 由于初始化单例对象 new DCLLazySingleton() 操作 并不是原子操作,由于这是很多条指令,jvm 可能会乱序执行。
在线程 1 初始化对象可能并未完成,但是此时已经 instance 对象已经不为 null。(已经分配了内存,但是构造方法还未执行完【可能有一些属性的赋值未执行 】)
此时线程 2 再获取 instance 则不为 null 直接返回。那么此时线程 2 获取的则为‘构造方法未执行完的 instance 对象’。则不能保证线程安全。
-
解决方式:
- 加上 volatile 关键字,volatile 保证内存可见性,内存屏障,防止指令排!
- 加上 volatile 关键字后,线程 2 获取的构造方法未执行完的 instance 对象,会在线程 1 修改之后同步到线程 2(volatile 内存空间)。所以解决了线程安全问题
-
参考:
- DCL 失效原因和解决方案
- java 中单例模式 DCL 的缺陷及单例的正确写法
4. 懒汉式(静态内部类)
public class StaticSingleton {
// 私有构造器
private StaticSingleton() {System.out.println("create StaticSingleton!");
}
// 获取单例的方法
public static StaticSingleton getInstance() {return SingletonHolder.instance;}
// 静态内部类 持有单例 作为静态属性。// 由于只有在访问属性时才会加载静态类初始化 instance。所以实现了懒加载。且由于 JVM 保证了类的加载为线程安全,所以为线程安全的。private static class SingletonHolder {
// 私有单例属性
private static StaticSingleton instance = new StaticSingleton();}
}
注意:
- 由于 StaticSingleton 类被加载时,内部的私有静态类 SingletonHolder 并不会被加载,所以并不会初始化单例 instance,当 getInstance()被调用时 SingletonHolder.instance 才会加载 SingletonHolder,由于 JVM 保证了类的加载为线程安全,因此线程安全。
- 此方式既可以做到延时加载,也不会因为同步关键字影响性能。是一种比较完善的实现。推荐使用
5. 枚举单例
public enum EnumSingleton {INSTANCE();
EnumSingleton() {System.out.println("create EnumSingleton");
}
}
- 线程安全,且能够抵御反射与序列化。
- 推荐使用
例外情况
上述的单例实现方式还是会面临一些特殊情况不能保证唯一实例:
- 反射调用私有构造方法。
- 序列化后反序列化会生成多个对象。可以实现私有 readResolve 方法。readObject()如同虚设,直接使用 readResolve 替换原本返回值。如下:
private Object readResolve () {
// 返回当前对象
return instance;
}
由于上述两情况比较特殊,所以没有特别关注。
参考书籍
《Java 程序性能优化》- 葛一鸣 等编著
正文完