前言
单例模式确保一个类只有一个实例,并提供一个全局访问点。
单例模式用于系统中创建的对象保证单一。单例模式很好理解,保证在系统中一个对象只实例化一次,但是在实现上有多种方式。
1. UML
2. 实现方式
实现上比较关键的是我们需要将构造函数访问属性设置成私有private。那么外部就不可以随便的使用new Object() 来实例化这个对象了。设置静态的单例对象(private static Singleton singleton)。
然后向外部提供一个全局访问点,在使用实例时调用此方法。
那么我们在何时进行实例化呢,是在使用时进行实例化,还是系统刚开始运行时就进行实例化对象?
总的来说分为饿汉模式和懒汉模式。懒汉模式比较懒,在使用时进行创建。饿汉模式在使用前就初始化这个对象,如果这个对象创建需要很多资源而且比较费时,我们需要延迟加载,在使用时再进行创建。总结饿汉和懒汉式的区别就是延迟加载。
2.1 懒汉
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}
上面的代码实现是线程不安全的,换句话说就是当多个线程同时访问时可能会new出来多个对象。
加锁使懒汉模式更安全:
public synchronized static LazySingleton getInstance() {
...
}
在获取实例的时候进行加锁,线程安全,但是这样的方式在获取实例性能比较低,每次获取实例都要加锁。(在静态方法中加 synchronized 先当于锁了这个类)
double check 双重检查的方式:
public class LazySingleton {
// volatile 防止指令重排序
private volatile static LazySingleton singleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (singleton == null) { // 第一次检查
synchronized (LazySingleton.class) {
if (singleton == null) { // 第二次检查 防止多个线程通过第一次检查
singleton = new LazySingleton();
// volatile 防止 new LazySingleton() 进行重排序 (防止 2 和 3 进行重排序) 如果不防止重排序 可能其他线程会通过第二次检查
// new LazySingleton() 进行了三个步骤
// 1. 分配内存给这个对象 2.初始化这个对象 3.设置 singleton 指向刚分配的内存地址
}
}
}
return singleton;
}
}
这样的方式我们在获取的时候,只在实例化的时候进行加锁。提高性能。注意实现的两点:双重检查和volatile关键字。对于重拍序有两种解决方案 不允许进行重排序的方式来实现的。还有一种方式是允许重排序的存在但是不让其他的线程看到这个线程的重排序。
通过静态内部类:
public class StaticInnerClassLazySingleton {
private StaticInnerClassLazySingleton(){
}
private static class InnerClass{
private static StaticInnerClassLazySingleton singleton = new StaticInnerClassLazySingleton();
}
public static StaticInnerClassLazySingleton getInstance(){
return InnerClass.singleton;
}
}
基于类初始化的延迟加载的解决方案。多个线程通过获取这个内部类Class对象的初始化锁。获取到锁后进行初始化,非构造线程是不能看到这个重排序的。
todo
2.2 饿汉
饿汉式的简单实现:
public class HungrySingleton {
private HungrySingleton(){}
// 在类加载就进行初始化 可以设置为final
private static final HungrySingleton singleton = new HungrySingleton();
public static HungrySingleton getInstance(){
return singleton;
}
}
写法简单,也避免线程安全问题。也可以把初始化的过程放在 static 代码块中,在类加载完成时就进行了赋值。
3. 破坏单例模式
序列化与反序列化安全:
- 如果获取到一个单例对象后,序列化到一个文件中,然后在从文件中取出来。那原来的对象和现在取得的对象还是同一个对象吗
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance(); //当 HungrySingleton 实现 Serializable 序列化接口时
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton-file"));
oos.writeObject(instance);
File file = new File("singleton-file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance == newInstance);
}
解决方案:在 HungrySingleton 中 加入一下代码
private Object readResolve() {
return singleton;
}
重点在于 ois.readObject() 中的 readObject0() 方法 中
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
readOrdinaryObject()方法中
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} ...
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
readResolve方法 在反序列化时通过反射返回同一个对象。
防止反射攻击:
- 通过反射获取到私有构造器,然后改变私有构造器的修饰符,从而 new 出新的对象
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class objectClass = HungrySingleton.class;
// getDeclaredConstructor 获得声明的构造器
Constructor constructor = objectClass.getDeclaredConstructor();
// 修改其权限
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
}
防止反射攻击之对于基于类加载的时候创建初始化对象的方式:在私有构造器中进行逻辑判断
private HungrySingleton() {
if (singleton != null) {
throw new RuntimeException("单例模式的私有构造器禁止反射调用");
}
}
对于不是类加载的时候创建单例对象的时候:是不能防止反射攻击的即使在私有构造器中加入标志 flag 进行判断是否进行实例化 也能被破坏。
通过枚举实现:推荐的单例模式。防止序列化破坏和反射攻击。属于饿汉模式。
public enum EnumInstance {
INSTANCE;
// 单例附带的数据
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
其他实现
基于容器的单例模式:
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> singletonMap = new HashMap<>();
public static void putInstance(String key, Object instance) {
if (StringUtils.isNotBlank(key) && instance != null) {
if (!singletonMap.containsKey(key)) {
singletonMap.put(key, instance);
}
}
}
public static Object getInstance(String key) {
return singletonMap.get(key);
}
}
单例对象多,用一个容器进行管理,节省资源,统一管理。线程不安全。
基于ThreadLocal 的方式的单例:注意此方式不是全局唯一 而是局部唯一,独立线程内的实例是唯一。
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> threadLocalInstance =
new ThreadLocal<ThreadLocalInstance>() {
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance() {
}
public ThreadLocalInstance getInstance() {
return threadLocalInstance.get();
}
}
其他问题
多个类加载器对单例模式的影响?
- 每个类加载器都定义了一个命名空间,如果有两个以上的类加载器,不同的类加载器可能会加载同一个类,从整个程序来看,同一个类会被加载多次。如果你的程序有多个类加载器又同时使用了单件模式,请自行指定类加载器,并指定同一个类加载器。
Spring 中的单例 如何保持 Singleton ?
- Spring的单例是基于容器的。Spring的单例是Bean作用域中的一个,这个作用域在每个应用程序上下文中仅创建一个。设置单例属性的实例和现在说的设计模式有点不一样。区别于:Spring将实例的数量限制的作用域在整个应用程序的上下文,现在写的单例模式在Java应用程序中将这些实例的数量限制在给定的类加载器管理的给定的空间里。
- 简单说单例模式是指在一个jvm进程中仅有一个实例,而Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。Spring的单例Bean是与其容器(ApplicationContext)密切相关的,所以在一个JVM进程中,如果有多个Spring容器,即使是单例bean,也一定会创建多个实例
AbstractFactoryBean中的 getObject() 判断是否是单例的。