单例模式的核心在于维护某个类最多只包含一个实例,并且该实例只能由类自己来创建,对外提供接口获取唯一对象
饿汉式实现:在系统启动时创建唯一实例对象,缺点在于影响项目启动速度
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getSingleton() {
return singleton;
}
}
通过静态内部类实现饿汉式:
public class InsideSingleton {
private static class inner {
private static final InsideSingleton singleton = new InsideSingleton();
}
private InsideSingleton() {
}
public static final InsideSingleton getInstance() {
return inner.singleton;
}
}
通过枚举实现,天然保证单实例:
public enum EnumSingleton {
singleton;
public void method() {
}
}
懒汉式实现:系统用到时访问,如果还没有就先创建,注意加锁防止初始化多次
public class LazySingleton {
private static volatile LazySingleton singleton = null;
private LazySingleton() {
}
public static synchronized LazySingleton getSingleton() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}
懒汉式实现优化,不对整个方法加锁,采用双重校验锁:
public class LazySingleton2 {
private static volatile LazySingleton2 singleton = null;
private LazySingleton2() {
}
public static LazySingleton2 getSingleton() {
if (singleton == null) {
synchronized (LazySingleton2.class) {
if (singleton == null) {
singleton = new LazySingleton2();
}
}
}
return singleton;
}
}
注意双重校验锁实现必须通过 volatile 修饰,不禁止指令重排序可能报错,这个最后介绍
spring IOC 实际就用到单例模式,被 Spring 管理的类只包含唯一实例,用到时动态注入。简单总结:
- 构造方法必须私有,防止外部初始化
- 必须包含静态获取实例方法
其它方面根据具体场景选择:饿汉式影响启动,懒汉式延迟加载,需保证同步,各有利弊。
关于双重校验锁为何还要用 volatile 修饰:
对象初始化可以粗略分为以下三步:
- 申请内存
- 创建对象
- 将引用指向对应内存
由于重排序的存在,上述步骤二三可能会颠倒,即先将引用指向对应内存,再将对象信息赋值到对应内存。此时假设不用 volatile 修饰,可能存在对象判断不为空,但实际还没有赋值的场景。此时返回引用,一旦调用方法就报错,因为对象实际还没有初始化完成,通过 volatile 禁止重排序就可以解决该问题