动力节点首页 全国咨询热线:400-8080-105

绑定手机号,登录
手机号

验证码

微信登录
手机号登录
手机号

验证码

30天自动登录
微信登录与注册
微信扫码登录与注册

扫码关注微信公众号完成登录与注册
手机号登录
首页 > 文章

slf4j框架源码中是如何实现双重锁的?

03-29 15:08 311浏览
举报 T字号
  • 大字
  • 中字
  • 小字

本篇文章我们就从使用日志框架最常见的入口类及方法入手。当我们使用slf4j时,通常通过如下代码获取对应的Logger:

Logger logger = LoggerFactory.getLogger(NoBindingTest.class);

在LoggerFactory的getLogger方法中,最主要的功能就是获得Logger,获得Logger需要先获得对应的ILoggerFactory,而ILoggerFactory又是通过SLF4JServiceProvider初始化和返回的。

本文重点聊聊获取和初始化SLF4JServiceProvider过程中使用到的双重锁机制。

双重锁实现源码

在LoggerFactory类最后有一个名为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)时再进行加锁,然后调用初始化操作。但此时如果初始化操作比较耗时,两个判断时都是未初始化,都进行初始化操作,只不过一先一后,就有可能初始化两次。

此时,进入锁之后,再进行一次判断,如果是未初始化再进行初始化,由于此时已经进入了锁内部,判断不会存在并发情况(这里并不完全准确,还涉及到指令重排情况),那么就避免了初始化两次的情况。

同时,经过第一次初始化之后,再次获取单例对象时,每次判断都不符合初始化的条件,也就不会走锁的逻辑,大大提高了并发。

整个双重锁的实现步骤便是:1、判断是否符合初始化条件;2、加锁当前类;3、再次判断是否符合初始化条件;4、初始化。

单例模式中的双重锁

slf4j框架源码中的双重锁主要是用来初始化SLF4JServiceProvider对象,基本上就是我们在实践或面试过程中经常提到的单例模式

而且通过slf4j的源码可以看出此处的单例模式属于懒汉模式,也就是只有当我们第一次调用LoggerFactory#getLogger方法时才会进行初始化。

下面以一个简单的单例模式再回顾一下双重锁的实现示例:

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;
    }
}

在上述代码当中我们看到Singleton变量使用到了volatile进行修饰。这是因为synchronized并不是对instance实例进行加锁(此时还并没有实例),所以在线程执行完初始化赋值操作之后,应该将修后的instance立即写入主内存(main memory),而不是暂时存在寄存器或者高速缓冲区(caches)中,以保证新的值对其它线程可见。

另外在上述单例模式中,new指令并不是原子操作,一般分为三步:1、分配对象内存;2、调用构造器方法,执行初始化;3、将对象引用赋值给变量。

而虚拟机在执行的时候并不一定按照上面1、2、3步骤进行执行,会发生“指令重排”,那就有可能执行的顺序为1、3、2。那么,第一个线程执行完1、3之后,第二个线程进来了,判断变量已经被赋值,就直接返回了,此时会便会发生空指针异常。而当对象通过volatile修饰之后,便禁用了虚拟机的指令重排。

因此,此处volatile是必须添加的,有两个作用:保证可见性和禁止指令重排优化。

回到slf4j中返回的成功初始化的对象PROVIDER时,PROVIDER变量对应的定义同样使用了volatile关键字修饰:

static volatile SLF4JServiceProvider PROVIDER;

如果面试单例模式,你能回答到单例模式的双重锁已经很不错了,但如果还能说清楚待实例化的变量使用volatile修饰的原因,那就完美了。认真阅读源码,还是有所收获的吧。

小结

本篇文章带大家初步了解了slf4j框架中双重锁实现的案例,同时分析了基本的原理与机制。

0人推荐
共同学习,写下你的评论
0条评论
代码小兵316
程序员代码小兵316

16篇文章贡献58453字

相关课程 更多>

作者相关文章更多>

推荐相关文章更多>

Java面试题及答案整理

代码小兵66904-21 20:01

6道经典算法面试题

杨晶珍05-12 16:39

简述Spring MVC的核心组件

代码小兵49806-11 16:26

SpringMVC 中的组件

代码小兵49806-11 16:28

Spring常见面试题

代码小兵92504-17 16:07

发评论

举报

0/150

取消