单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
注意:
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。
单例类必须给所有其他对象提供这一实例。
1. 饿汉式(静态变量)
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
保证单例的点:静态变量在类加载时初始化,只初始化一次。
线程安全:因为类加载本身就是线程安全的。
2. 饿汉式(静态代码块)
public class Singleton {
private static final Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
保证单例的点:和上面一样,只是初始化写到了静态代码块里。
3. 懒汉式(线程安全同步方法 )
public class Singleton {
private static Singleton instance;
private Singleton4() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
保证单例的点:synchronized保证同一时刻只有一个线程能进入方法。
缺点:每次都同步,性能差。
4. 双重检查锁(DCL)
public class Singleton {
private static volatile Singleton instance; // volatile 一定要有
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 还没创建则创建
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 拿到锁的时候可能有其他线程先拿到锁已经创建了,这个时候再判断一次是否已经创建
instance = new Singleton();
}
}
}
return instance;
}
}
保证单例的点:
外层第一次检查instance == null避免加锁开销;
内层加锁后再检查一次,确保只有一个实例;
volatile禁止指令重排序,防止半初始化问题。
5. 静态内部类方式
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
保证单例的点:
类加载是懒加载的,Holder类不会初始化,直到调用getInstance();
JVM保证类初始化是线程安全的。
6. 枚举单例
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Doing something...");
}
}
保证单例的点:
JVM保证每个枚举实例在内存中只创建一次;
防止反射攻击;
防止反序列化破坏单例。
7. Spring容器管理的单例
在Spring里,默认情况下,Bean是单例的(@Scope("singleton")):
@Service
public class MyService {
}
保证单例的点:
Spring在IOC容器初始化阶段实例化Bean,并放到单例池;
后续通过名字拿对象,拿的都是同一个。
8. 单例模式的常见问题
8.1反射破坏单例
反射可以访问私有构造器,从而绕开正常流程,直接创建多个实例。
Singleton instance1 = Singleton.getInstance();
Constructor
constructor.setAccessible(true);
Singleton instance2 = constructor.newInstance();
System.out.println(instance1 == instance2); // false!
可以在构造方法中加一个判断,防止二次实例化:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象,禁止反射创建!");
}
}
public static Singleton getInstance() {
return INSTANCE;
}
}
这样即使通过反射调用,也会抛异常!
8.2 序列化破坏单例
如果单例对象实现了Serializable接口,在序列化 -> 反序列化过程中,会创建出一个新的对象实例。
Singleton instance1 = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
oos.writeObject(instance1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));
Singleton instance2 = (Singleton) ois.readObject();
System.out.println(instance1 == instance2); // false!
可以加一个 readResolve() 方法:
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
这样反序列化时不会新建对象,而是直接返回已有的实例!
规则:
readResolve()方法在ObjectInputStream读取对象后自动调用
返回的对象替换掉反序列化出来的新对象
为什么推荐枚举单例
枚举类的实例创建由JVM保证,天然防止了:
反射破坏
反序列化破坏
多线程破坏
总结
序号
实现方式
保证单例的机制
是否懒加载
线程安全
备注
1
饿汉式(静态变量)
类加载时创建
否
是
简单,推荐
2
饿汉式(静态代码块)
类加载时创建
否
是
可加复杂逻辑
3
懒汉式(线程安全同步方法)
延迟初始化+同步方法
是
是
性能差
4
懒汉式(DCL双重检查)
volatile + 双检查锁
是
是
推荐,最经典
5
静态内部类
JVM加载机制
是
是
推荐,优雅
6
枚举单例
JVM保证枚举单例
否
是
最推荐,防反射和反序列化
7
Spring容器管理单例
Spring的IOC容器
可配置
是
Spring默认就是单例Bean