设计模式 - 单例模式
1. 在Java中实现单例模式有哪些方法
单例模式是指一个类在任何情况下只有一个实例,并且提供全局访问点来获取该实例。
要实现单例模式,需满足
- 私有化构造方法,防止被外部实例化造成多实例问题;
- 提供一个静态方法作为全局访问点来获取唯一的实例对象。
实现单例的方法:
延迟加载
通过延迟加载的方式进行实例化,并增加了同步锁机制,避免多线程下的线程安全问题。
public class Singleton {
private static Singleton instance;
// 私有化构造函数,防止外部实例化
private Singleton() {
// 初始化代码
}
// 提供一个公共的静态方法来获取实例
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
缺点:有性能问题,同步锁只在第一次实例化时有用,后续不需要。
双重检查锁
双检锁来缩小锁的范围以提升性能
public class Singleton {
private static volatile Singleton instance;
// 私有化构造函数,防止外部实例化
private Singleton() {
// 初始化代码
}
// 提供一个公共的静态方法来获取实例
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
饿汉式
在类加载时就触发了实例化,从而避免多线程同步问题。
public class Singleton {
private static Singleton instance = new Singleton();
// 私有化构造函数,防止外部实例化
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
静态代码块
通过静态代码实例化,也能在类加载时触发并且只执行一次。
public class Singleton {
private static Singleton instance = null;
static {
// 静态代码块用于初始化单例实例
instance = new Singleton();
}
// 私有化构造函数,防止外部实例化
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
以上两种都是饿汉式,缺点是会降低启动速度,造成资源浪费。
静态内部类
把 INSTANCE 写在一个静态内部类中,由于只有在调用静态内部类的方法、静态域或者构造方法的时候才会加载静态内部类,所以当Singleton 被加载的时候不会初始化 INSTANCE,从而实现了延迟加载。
不存在线程安全问题
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE; // 首次调用时触发类加载
}
}
枚举类
既是线程安全的,又能防止反序列化导致的破坏单例的问题。
// 枚举实现单例(隐式延迟加载)
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务逻辑
}
}
2. 哪些情况下的单例对象可能会被破坏
多线程
单例对象作为共享资源可能被多个线程同时操作,从而导致创建了多个对象。
可能出现在懒汉式单例中。
解决方案
- 使用双检锁
- 使用静态内部类
指令重排
instance = new Singleton()
这条赋值语句会被转换为3条指令,指令重排后,instance 指向分配好的内存的指令被排在了前面。在T1线程初始化这段内存之前,T2线程虽然不在同步代码块中,但是在同步代码块之前进行判断,会发现 instance 不为空,此时线程T2获得 instance 对象,直接使用就可能发生错误。
解决方案:在成员变量前加上 volatile, 保证所有线程可见性。
private static volatile Singleton instance = null;
克隆
Java中所有的类都继承自Object,都实现了clone() 方法,如果是深克隆,每次都会创建新的实例。
解决方法:重写clone() 方法,将单例自身的引用作为返回值。
反序列化
解决方法:在反序列化的过程中,Java API 会调用 readResolve() 方法,可以重写 readResolve() 方法,将返回值设置为已经存在的单例对象。
反射
解决放案:
- 使用枚举实现单例:枚举类的构造函数会被 JVM 严格限制,反射无法调用。
- 在构造方法中检查对象是否已被创建
private Singleton() {
if (INSTANCE != null) {
throw new IllegalStateException("单例已存在,禁止反射创建!");
}
// 初始化逻辑
}
3. 在DLC单例写法中,为什么主要做两次检查
第一次检查是为了保证只有首次并发时才出现阻塞,以此提高性能;第二次检查时为了保证不会重复创建对象;加锁以保证线程安全。
4. 哪些场景不适合使用单例模式
适用的场景:如果某个共享资源使用频次非常高,而且不可替代性也很强,就可以被设计为单例。比如,Spring IoC容器,JDK的Runtime.
不适用场景:要经常被赋值传递的对象 VO, POJO 等。