单例(Singleton)模式要求一个类有且仅有一个实例,并且提供了一个全局的访问点。这就提出了一个问题:如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?客户程序在调用某一个类时,它是不会考虑这个类是否只能有一个实例等问题的,所以,这应该是类设计者的责任,而不是类使用者的责任。 从另一个角度来说,Singleton模式其实也是一种创建型模式。因为我们创建了一个对象,这个对象扮演了独一无二的角色,在这个单独的对象实例中,它集中了它所属类的所有权力,同时它也肩负了行使这种权力的职责。
单例应用场景举一个小例子,我们在Windows桌面上打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你再弹出一个新的回收站窗口,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例,这就是一个典型的单例模式运用。
继续说回收站,我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如每次创建回收站时都需要消耗大量的资源,那么重复创建了多个回收站实例,这样就给系统造成不必要的负担和资源浪费。
单例模式的优点在内存中只有一个实例,减少了内存开销,可以避免对资源的多重占用,设置全局访问点严格控制访问。
单例模式的缺点没有接口,扩展困难,如果要扩展单例对象,只能通过修改代码,违背开闭原则。
单例的定义:- 构造器私有化(拒绝被其他类构造实例,只能通过当前类本身创建实例并对外提供)。
- 定义一个私有静态变量存储当前类的实例。
- 提供一个公有静态方法对外提供当前类实例。
public class HungerSingleton {
// 类加载阶段赋值
private static final HungerSingleton INSTANCE = new HungerSingleton();
private HungerSingleton() {}
public static HungerSingleton getInstance() {
return INSTANCE;
}
}
饿汉模式是比较常见的一种写法,在类加载的时候就对实例进行初始化,没有线程安全问题,调用效率高。
缺点:由于static变量会在类加载阶段被赋值,如果项目中有很多饿汉式单例模式的类,会造成内存浪费,例如Spring中的对象默认是单例,如果采用饿汉式单例,影响可想而知。
2.懒汉模式 - 常规版public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉模式也是常见的单例实现方式之一,优点是会在第一次使用的时候才进行实例创建。需要注意的是,这种模式需要通过加锁来保证线程安全,最简单的方式是将获取实例的方法通过synchronized关键字定义成同步方法,这种写法的缺点是每次获取实例都要进行同步(加锁),因此效率较低。
3.懒汉模式 - 双重检测机制(DCL)public class LazySingleton {
// volatile防止指令重排序
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
双重检测机制,在方法内部使用同步块,相比同步方法的实现方式更严谨,相较而言效率有所优化。
上例中实例变量被volatile修饰,是为了防止指令重排序,因为在java中创建对象并非原子操作,正常情况下被分解成三个指令:
1:分配对象的内存空间:memory = allocate();
2:初始化对象 :ctorInstance(memory);
3:设置instance指向刚分配的内存地址:instance = memory;
因为编译器或处理器为提升性能,在不影响最终结果的前提下(这里指某一个线程的执行结果),可能会改变内部指令的执行顺序,例如:1+2+3,执行的时候可能是1+3+2,最终结果都是一样的。
所以new LazySingleton()可能执行顺序是这样的:
1:分配对象的内存空间:memory = allocate();
3:设置instance指向刚分配的内存地址:instance = memory;
2:初始化对象:ctorInstance(memory);
基于这种情况,当线程A进行实例化时,执行完1、3准备执行2时,刚好切换到线程B并执行了第一个检查if (instance == null) ,这时instance已经不再指向null,导致最终线程B拿到的就是一个未初始化的实例。
4.懒汉模式 - 静态内部类(延迟初始化占位类)public class LazySingletonInnerClass {
private LazySingletonInnerClass() {
if (InnerHolder.instance != null)
throw new RuntimeException("请勿非法访问");
}
// 防止被序列化破坏
private Object readResolve() {
return InnerHolder.instance;
}
public static LazySingletonInnerClass getInstance() {
return InnerHolder.instance;
}
static class InnerHolder {
private static final LazySingletonInnerClass instance = new LazySingletonInnerClass();
}
}
静态内部类,JVM将推迟InnerHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化LazySingletonInnerClass,因此不需要额外的同步。当任何一个线程第一次调用getInstance时,都会使InnerHolder被加载并初始化,此时静态初始化器将执行LazySingletonInnerClass的初始化操作。
通过静态初始化来初始化LazySingletonInnerClass为什么不需要额外的同步?
在初始器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。所以无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。
注:构造方法中的判断if (InnerHolder.instance != null)是为了防止单例模式被反射破坏。readResolve()方法是为了防止被序列化破坏。
5.容器式单例public class ContainerSingleton {
private static Map ioc = new ConcurrentHashMap<>();
private ContainerSingleton() {}
public static Object getInstance(String beanName) {
if (!ioc.containsKey(beanName)) {
synchronized (ioc) {
if (!ioc.containsKey(beanName)) {
try {
Object obj = Class.forName(beanName).newInstance();
ioc.put(beanName, obj);
return obj;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
return ioc.get(beanName);
}
}
容器式单例是为了解决需要大批量创建单例的场景,例如Spring。
单例模式总结- 私有化构造器
- 保证线程安全
- 延迟加载
- 防止序列化反序列化破坏单例
- 防御反射攻击单例



