【java】深度解析单例模式
前言
关于单例模式,请看菜鸟教程中的定义:
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
代码实现
饿汉模式
package com.dl.hungry_singleton;
public class HungrySingleton {
// 在类加载的时候,单例对象就已经被创建
private static final HungrySingleton instance = new HungrySingleton();
// 构造方法私有化
private HungrySingleton() {
}
// 提供一个全局的访问点
public static HungrySingleton newInstance() {
return instance;
}
}
下面这种实现方法,与上面的代码本质上没有差别。
package com.dl.hungry_singleton;
public class HungrySingleton {
private static final HungrySingleton instance;
// 通过静态块来初始化
static {
instance = new HungrySingleton();
}
// 构造方法私有化
private HungrySingleton() {
}
// 提供一个全局的访问点
public static HungrySingleton newInstance() {
return instance;
}
}
- 优点:简单,易读,能够保证绝对的线程安全;
- 缺点:饿汉模式会浪费内存,如果在整个开发系统中有大量的饿汉模式的单例对象,那么对系统来说是一种灾难。
- 解决办法:使用懒汉模式;
懒汉模式
package com.dl.lazy_singleton;
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton newInstance() {
// 第一次使用该对象,需要进行初始化
if (null == instance) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
对于稍有并发编程基础的同学来说,很容易看出来上面的单例模式在多线程中是存在问题的。测试代码博主就不列出来了,有兴趣的同学可以自行测试。下面仅作出结论并验证结论。
- 多线程调用newInstance()方法返回值相同
- 多个线程顺序执行,线程安全;
- 若非顺序执行,则线程不安全;① 假设有两个线程都通过了 if 判断条件,并执行了instance = new LazySimpleSingleton()语句,各自拥有了自己的对象 ② 两个线程依次在执行return instance语句前时间片段到,进入就绪态 ③ 两个线程依次返回对象。结果是两个线程得到的对象将是同一个,因为后一个线程new出的对象覆盖了前一个线程new出的对象,虽然表面看是同一个对象,但实则线程并不安全。
- 多线程调用newInstance()方法返回值不同
- 当每个线程都通过了 if 判断条件,且每个线程new对象的过程与return instance的过程没有被打断时,那么每个线程得到的对象的值都不同。
- 优点:解决了饿汉单例模式的内存浪费问题
- 缺点:线程不安全
- 解决办法:给方法加锁,但在高并发系统中会产生大量的阻塞线程,性能很差
双重检查锁
package com.dl.lazy_singleton;
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton() {
}
public synchronized static LazyDoubleCheckSingleton newInstance() {
// 外层检查,控制阻塞的条件,用来提高性能
if (null == instance) {
synchronized(LazyDoubleCheckSingleton.class) {
// 内层检查,控制创建实例的条件
if (null == instance) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
虽然这里也使用了synchronized关键字来加锁,会增加初次初始化单例对象时的性能消耗;但毕竟在系统运行过程中,创建单例对象在使用过程中所占的时间比例很小,所以加锁所造成的代价是值得的;况且仅在系统刚运行时使用多线程获取单例对象时才有可能会出现锁消耗。
- 优点:保证了线程安全,也提高了系统性能
- 缺点:可读性差,代码不够优雅,双 if 判断着实难以理解
静态内部类
package com.dl.lazy_singleton;
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
}
// 这里的方法是静态方法,因此内部类必须是静态内部类
public static LazyInnerClassSingleton getInstance() {
return LazyInnerSingleton.LazySingleton;
}
private static class LazyInnerSingleton {
private static final LazyInnerClassSingleton LazySingleton
= new LazyInnerClassSingleton();
}
}
相信第一次看到这种单例模式的同学应该会有点懵,下面揭开谜底;
类加载时机:类加载的其中一个时机是,当类中的静态属性或静态方法被调用时,会先判断该类是否已被加载,如果没有,则先加载该类。
静态内部类:当只调用外部类的静态属性,静态方法时,静态内部类不会被加载;只有当其它类或外部类调用静态内部类中的静态属性或静态方法时才会加载静态内部类。
根据静态内部类的特性就可以实现懒加载机制。
- 优点:代码简单且线程安全,线程安全由jvm的类加载机制保证
- 缺点:前五个单例模式的实现都存在一个共同的缺陷;private权限的构造器无法阻止利用反射机制去创建对象。
- 解决办法:在构造器中抛异常,但这并不符合编程的基本规则。
private LazyInnerClassSingleton() {
if (LazyInnerSingleton.LazySingleton != null) {
throw new RuntimeException("Cannot reflectively create enum objects");
}
}
在Java API中有一种特殊的类,叫做枚举(enum),它自带上面代码中的这个功能。
注册式单例-枚举类
package com.dl.register_singleton;
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
枚举类是一个封装的类,像数组一样由jdk内部来实现,博主通过伪代码来大概解释一下:
// 上面枚举类的实质如下
class EnumSingleton extends Enum {
public static final EnumSingleton INSTANCE;
}
// 在jvm内部存在一个由系统维护的 map<String, Object> 来保存该类所有的枚举类对象
// map.put("INSTANCE", INSTANCE);
所以这也相当于第一种单例模式的变形,只不过枚举类单例模式可以保证不会被反射机制破坏。
下面通过测试来验证枚举类是否可以通过反射机制来创建对象
// 枚举类只有一个构造器,所以在测试时需要注意
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
package com.dl.register_singleton;
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) {
Class<?> klass = EnumSingleton.class;
try {
// 注意这里的构造器只有一个
Constructor<?> constructor
= klass.getDeclaredConstructor(String.class, int.class);
// 设置构造器的权限
constructor.setAccessible(true);
// 反射机制创建对象
Object obj = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.dl.register_singleton.Test.main(Test.java:12)
很明显,我们可以看到jdk不允许用户通过反射机制来构造枚举类对象,让我们一起来看一下Constructor.java:417的源码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
至此,可以得出结论:枚举式单例的饿汉模式很好的保证了线程安全性问题,且可以保证不会被反射机制破坏,这一点由jdk为我们保驾护航。因此,枚举式单例模式应该是目前为止最好的实现手法了。
容器式单例模式
package com.dl.register_singleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ContainerSingleton {
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
private ContainerSingleton() {
}
@SuppressWarnings("unchecked")
public static <T> T getInstance(String className) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className);
ioc.put(className, obj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return (T) obj;
} else {
return (T) ioc.get(className);
}
}
}
- 优点:枚举式单例模式的升级(饿汉->懒汉),枚举式单例中的map由jdk来控制,而容器式单例模式的map由用户类来维护,显得更灵活;
- 缺点:会被反射机制破坏;
容器式单例模式其实是一种另外的设计思路,当开发的系统中存在多个单例对象时,可以通过这种模式来保存,典型的例子就是Spring-ioc底层javaBeanPool的实现,关于这部分可以看博主的模拟实现Spring-ioc,这里不再赘述。
ThreadLocal单例
package com.dl.threadLocal_singleton;
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocal =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
public ThreadLocalSingleton get() {
// 通过new对象获得,因为每个线程得到的对象都不同
return new ThreadLocalSingleton();
};
};
private ThreadLocalSingleton() {
}
public ThreadLocalSingleton getInstance() {
return threadLocal.get();
}
}
对于ThreadLocal源码的解析,博主后期可能会出一篇博文来进行全面讲解。
这种单例模式是可以避免加锁来实现同步的,在高并发下,可以保证每一个线程都拥有自己独有的对象,因此这也并不是单例模式实现的典型思路。这种方式,虽不能保证全局唯一,但能保证线程唯一。比较好的应用场合就是配合容器式单例模式使用,仅对对象中的单例对象(map等)进行操作,这样就可以保证多线程的一致性。
- 优点:保证线程安全,避免加锁带来的性能消耗,代码简单;
- 缺点:用空间换时间的代价是浪费内存,也会被反射机制破坏;
总结
本博文一共总结了7种典型的单例模式,没有哪一种是绝对好或绝对差的,通过不同的场景来应用不同的模式是编程者应该具有的最基本的素质。