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;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
2022-10-26 IDEA 跳到定义或使用
2021-10-26 AutoLISP command 命令中的 _.line line -line 有什么区别