单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建性模式。单例模式在开发中应用非常广泛,例如,Spring 框架中的 ApplicationContext、数据库的连接池等。
单例模式有如下实现方式:
饿汉式
懒汉式
注册式
单例模式在实现的时候需要考虑如下两个问题:
多线程环境下对单例模式的破坏
反射、序列化对单例模式的破坏
饿汉式单例模式
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。
Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例模式。
优点:没有加任何的锁、执行效率比较高,用户体验比懒汉式单例模式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。
懒汉式单例模式的写法如下所示:
public class HungrySingleton {
// 1.私有化构造器
private HungrySingleton (){}
// 2.在类的内部创建自行实例
private static final HungrySingleton instance = new HungrySingleton();
// 3.提供获取唯一实例的方法(全局访问点)
public static HungrySingleton getInstance(){
return instance;
}
}
还有另外一种写法,利用静态代码块的机制:
public class HungryStaticSingleton {
// 1. 私有化构造器
private HungryStaticSingleton(){}
// 2. 实例变量
private static final HungryStaticSingleton instance;
// 3. 在静态代码块中实例化
static {
instance = new HungryStaticSingleton();
}
// 4. 提供获取实例方法
public static HungryStaticSingleton getInstance(){
return instance;
}
}
这两种写法都非常的简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。下面我们来看性能更优的写法。
懒汉式单例模式
懒汉式单例模式的特点是:被外部类调用的时候内部类才会加载。
简单懒汉式(线程不安全)
下面来看懒汉式单例模式的简单实现 LazySimpleSingleton:
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
private static LazySimpleSingleton instance = null;
public static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
然后写一个线程类 ExectorThread:
public class ExectorThread implements Runnable {
@Override
public void run() {
LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);
}
}
客户端测试代码如下:
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("End");
}
}
运行结果如下图所示。
上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患。我们通过过时运行再具体看一下。这里教大家一种新技能,用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化。先给 LazySimpleSingleton 类打上断点,如下图所示:
然后鼠标右键单击断点,切换为 Thread 模式,如下图所示。
开始“Debug”之后,会看到 Debug 控制台可以自由切换 Thread 的运行状态,如下图所示。
分别选择 Thread-0 和 Thread-1,都执行一步,都进入到 if 判断中。
这样 LazySimpleSingleton 就被实例化了两次。
简单懒汉式(线程安全)
通过对上面简单懒汉式单例的测试,我们知道存在线程安全隐患,那么,如何来避免或者解决呢?
通过给 getInstance() 方法加上 synchronized 关键字,使这个方法编程线程同步方法:
public class LazySimpleSyncSingleton {
private LazySimpleSyncSingleton() {
}
private static LazySimpleSyncSingleton instance = null;
public synchronized static LazySimpleSyncSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSyncSingleton();
}
return instance;
}
}
我们再来调试。当执行其中一个线程并调用 getInstance() 方法时,另一个线程在调用 getInstance() 方法,线程的状态有 RUNNING 变成了 MONITOR,出现阻塞。知道第一个线程执行完,第二个线程才恢复到 RUNNING 状态继续调用 getInstance() 方法,如下图所示。
上图完美地展现了 synchronized 监视锁的运行状态,线程安全的问题解决了。但是synchronized 加锁时,在线程数量比较多的情况下,如果 CPU 分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。
双重检查锁懒汉式
那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式:
public class LazyDoubleCheckSingleton {
private LazyDoubleCheckSingleton() {
}
private static LazyDoubleCheckSingleton instance = null;
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();// error
}
}
}
return instance;
}
}
上面代码的执行顺序如下:
- 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回;
- 获取锁;
- 再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象;
执行双重检查是因为,如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。这样,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题。
上述写法看似解决了问题,但是有个很大的隐患。实例化对象的那行代码(标记为 error 的那行),字节码操作可以分解成以下三个步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但是有些编译器为了考虑性能,可能会对字节码命令重排序,将第二步和第三步进行重排序,顺序就成了:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
现在考虑重排序后,两个线程发生了以下情况的调用:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到 lazyThree 为空 | |
T2 | 获取锁 | |
T3 | 再次检查到 lazyThree 为空 | |
T4 | 为 lazyThree 分配内存空间 | |
T5 | 将 lazyThree 指向内存空间 | |
T6 | 检查到 lazyThree 不为空 | |
T7 | 访问 lazyThree(此时对象还未完成初始化) | |
T8 | 初始化 lazyThree |
在这种情况下,T7 时刻线程B 对 lazyThree 的访问,访问的是一个初始化未完成的对象。
正确的双重检查锁的写法如下所示:
public class LazyDoubleCheckSingleton {
private LazyDoubleCheckSingleton() {
}
private volatile static LazyDoubleCheckSingleton instance = null;
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
为了解决上述问题,需要在 instance 前加入关键字 volatile。使用了 volatile 关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
静态内部类懒汉式
用到 synchronized 关键字总归要上锁,对程序性能还是存在一定的影响。有没有更好的方案?当然有,我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
}
// 注意关键字final,保证方法不被重写和重载
public static final LazyInnerClassSingleton getInstance() {
// 在返回结果之前,会先加载内部类
return LazyHolder.INSTANCE;
}
// 默认不加载
private static class LazyHolder {
private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
}
}
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。
我们可以通过如下时序图来看一下调用顺序:
- 客户端调用 LazyInnerClassSingleton.getInstance(),此时会先判断 LazyInnerClassSingleton 这个类是否已经加载,如果没有加载则先加载,然后调用 getInstance 方法;
- getInstance 方法内调用了 LazyHolder.LAZY,则此时会先判断 LazyHolder 这个类是否已经加载,如果没有加载则先加载,并初始化自身的静态属性,此时 LAZY 通过 new LazyInnerClassSingleton() 完成了初始化;
- 返回 LazyHolder 的属性 LAZY 的引用,最终把引用返回到客户端;
从上面的流程逻辑,我们可以看到,内部类是在方法调用之前初始化,如果在 getInstance 方法中没有调用LazyHolder.LAZY,那么 LazyHolder 是不会完成初始化的,巧妙地避免了线程安全问题,同时节省了系统的开销。
注册式单例模式
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为枚举式单例模式,另一种为容器式单例模式。
枚举式单例模式
创建枚举类 EnumSingleton 类:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
来看测试代码:
public class EnumSingletonTest {
public static void main(String[] args) {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
try {
//序列化
FileOutputStream fos = new FileOutputStream("EnumSingletonTest.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("EnumSingletonTest.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下图所示。
没有做任何处理,我们发现运行结果和预期的一样。为什么枚举式单例模式能够避免反射对单例模式的破坏?下面通过分析源码来揭开它的神秘面纱。
下载一个 Java 反编译工具 XJad 打开 EnumSingleton.class 文件,看到有如下代码:
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
原来,枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例模式的实现。序列化能不能破坏枚举式单例其实在 JDK 源码中也有体现,我们继续回到 ObjectInputStream 的 readObject0() 方法:
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
...
case TC_ENUM:
return checkResolve(readEnum(unshared));
...
}
我们看到,在 readObject0() 中调用了 readEnum() 方法,来看 readEnum() 方法的代码实现:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
我们发现,枚举乐行其实通过类名和 class 对象找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么反射是否能破坏枚举式单例模式呢?来看一段测试代码:
private static void reflectionTest() {
try {
Class clazz = EnumSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
EnumSingleton singleton = (EnumSingleton) constructor.newInstance();
System.out.println(singleton);
} catch (Exception e) {
e.printStackTrace();
}
}
运行结果如下图所示。
结果中报的是 java.lang.NoSuchMethodException 异常,意思是没找到无参的构造方法。我们打开java.lang.Enum 的源码,发现只有一个 protected 的构造方法,代码如下:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
我们再来做一个下面这样的测试:
private static void reflectionTest() {
try {
Class clazz = EnumSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton singleton = (EnumSingleton) constructor.newInstance("tom", "666");
System.out.println(singleton);
} catch (Exception e) {
e.printStackTrace();
}
}
运行结果如下图所示。
结果中报错 Cannot reflectively create enum objects,意思是不能通过反射来创建枚举类,关于这个在 JDK 源码中也有说明,我们来看 Constructor 的 newInstance() 方法:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
从上述代码可以看出,在 newInstance() 方法中做了强制性的判断,如果修饰符是 Modifier.ENUM,则直接抛出异常。
枚举式单例也是《Effective Java》书中推荐的一种单例实现写法。JDK 枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例模式成为一种比较优雅的实现。
容器式单例模式
接下来看注册式单例模式的另一种写法,即容器式单例模式,创建 ContainerSingleton 类:
public class ContainerSingleton {
// 私有的构造方法
private ContainerSingleton(){}
// 存储实例的map,ConcurrentHashMap中线程安全,spring框架的IOC注册中心就是用这种方式实现的
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getBean(String className){
synchronized (ioc){
//如果map中没有这个class实例
if(!ioc.containsKey(className)){
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
else
return ioc.get(className);
}
}
}
容器式单例模式适用于实例非常多的情况,便于管理。
线程单例实现 ThreadLocal
线程单例使用 ThreadLocal 来实现。ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。下面来看代码:
public class ThreadLocalSigleton {
private static final ThreadLocal<ThreadLocalSigleton> threadLocalInstance = new ThreadLocal<ThreadLocalSigleton>(){
@Override
protected ThreadLocalSigleton initialValue() {
return new ThreadLocalSigleton();
}
};
private ThreadLocalSigleton(){};
public static ThreadLocalSigleton getInstance(){
return threadLocalInstance.get();
}
}
测试代码:
我们发现,在主线程中无论调用多少次,获得到的实例都是同一个;在多线程环境下,每个线程获取到了不同的实例。
单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal 将所有的对象放在 ThreadLocalMap 中,为每个线程都提供一个对象,这实际上是以空间换时间来实现线程间隔离的。
摘录:《Spring 5 核心原理与30个类手写实战》来自文艺界的Tom老师的书籍。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/gl0ge2 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。