SLF4J 中的单例模式
当我们使用 SLF4J 时,通常通过如下代码获取对应的 Logger:
Logger logger = LoggerFactory.getLogger(NoBindingTest.class);
在 LoggerFactory 的 getLogger 方法中,最主要的功能就是获得 Logger,获得 Logger 需要先获得对应的 ILoggerFactory:
而 ILoggerFactory 又是通过 SLF4JServiceProvider 初始化和返回的:
本文重点聊聊上图中 getProvider 方法获取和初始化 SLF4JServiceProvider 过程中使用到的基于双重校验锁的单例模式。
getProvider 源码
getProvider 方法的作用是返回当前正在使用的 SLF4JServiceProvider 实例。具体代码如下:
static SLF4JServiceProvider getProvider() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return PROVIDER;
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_PROVIDER;
}
throw new IllegalStateException("Unreachable code");
}
从上面的代码可以大概看出获取 SLF4JServiceProvider 分两步,第一步就是初始化,第二步就是通过 switch 来比对当前实例化的状态(或阶段),然后返回对应的实例对象或抛出异常。
其中第一步操作便使用到了双重校验锁。下面根据代码分析一下源码中双重校验锁的使用流程。
如果只是简单的使用锁机制,防止重复实例化 SLF4JServiceProvider 对象,直接在 getProvider 方法上添加 synchronized 便可。但这就面临性能问题,因为每次调用该方法时都是同步处理的。实际上只有第一次初始化时有加锁的必要。
那么此时可以将锁缩小范围,判断当前是否已经初始化,只有当未初始化(UNINITIALIZED)时才加锁,然后调用初始化操作:
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
但多线程情况下,可能多个线程INITIALIZATION_STATE == UNINITIALIZED
判断时都是未初始化,也就会有多个线程依次进入 synchronized 块进行初始化。
所以,进入锁之后,要再进行一次判断,如果是未初始化再进行初始化,由于此时已经进入了锁内部,判断不会存在并发情况(这里并不完全准确,还涉及到指令重排情况),那么就避免了初始化两次的情况:
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
同时,经过第一次初始化之后,再次获取单例对象时,每次判断都不符合初始化的条件,也就不会走锁的逻辑,大大提高了并发。
整个双重校验锁的实现步骤便是:1、判断是否符合初始化条件;2、加锁当前类;3、再次判断是否符合初始化条件;4、初始化。
单例模式中的双重校验锁
通过上面 SLF4J 的源码可以看出此处的单例模式属于基于双重校验锁的单例模式。
下面是基于双重校验锁的单例模式示例:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在上述代码当中我们看到 instance 变量使用到了 volatile 进行修饰。这是因为这里存在内存可见性的问题,也就是对 instance 进行赋值之后,并不会马上写入内存,期间对于其他线程来说,instance 还是未初始化,所以在线程执行完初始化赋值操作之后,应该将修改后的 instance 立即写入主内存(main memory),而不是暂时存在寄存器或者高速缓冲区(caches)中,以保证新的值对其它线程可见。
另外在上述单例模式中,new 指令并不是原子操作,一般分为三步:1、分配对象内存;2、调用构造器方法,执行初始化;3、将对象引用赋值给变量。
而虚拟机在执行的时候并不一定按照上面 1、2、3 步骤进行执行,会发生“指令重排”,那就有可能执行的顺序为 1、3、2。那么,第一个线程执行完 1、3 之后,第二个线程进来了,判断变量已经被赋值,就直接返回了,此时会便会发生空指针异常。而当对象通过 volatile 修饰之后,便禁用了虚拟机的指令重排。
因此,此处 volatile 是必须添加的,有两个作用:保证可见性和禁止指令重排优化。
回到 SLF4J,getProvider 方法中调用了 performInitialization 方法,performInitialization 中调用了 bind 方法,bind 方法中完成实例的获取,将实例赋值给 PROVIDER 属性:
可以看到 PROVIDER 属性同样使用了 volatile 关键字来修饰:
static volatile SLF4JServiceProvider PROVIDER;