首页 > 其他分享 >SLF4J 中的单例模式

SLF4J 中的单例模式

时间:2024-10-26 13:42:45浏览次数:1  
标签:UNINITIALIZED 初始化 INITIALIZATION STATE 模式 instance SLF4J 单例

基于:SLF4J 框架源码中是如何实现双重锁的?

当我们使用 SLF4J 时,通常通过如下代码获取对应的 Logger:

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

在 LoggerFactory 的 getLogger 方法中,最主要的功能就是获得 Logger,获得 Logger 需要先获得对应的 ILoggerFactory:

image-20200606161315662

而 ILoggerFactory 又是通过 SLF4JServiceProvider 初始化和返回的:

img

本文重点聊聊上图中 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 属性:

img

可以看到 PROVIDER 属性同样使用了 volatile 关键字来修饰:

static volatile SLF4JServiceProvider PROVIDER;

标签:UNINITIALIZED,初始化,INITIALIZATION,STATE,模式,instance,SLF4J,单例
From: https://www.cnblogs.com/Higurashi-kagome/p/18503955

相关文章

  • Java 单例模式
    原文:Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式?1、懒汉(线程不安全)publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}//私有构造函数publicstaticSingletongetInstance(){if(insta......
  • SLF4J 中的适配器模式
    什么是适配器模式适配器模式中,适配器包装不兼容指定接口的对象,来实现不同兼容指定接口。SLF4J中的适配器模式SLF4J是一个日志门面系统,其中提供了统一的Logger等接口,许多框架都会面向SLF4J打印日志,这样就不会和具体的日志框架耦合在一起,框架使用者也就能够很方便地在不同......
  • 前端开发设计模式——工厂模式
    目录一、定义和特点1.定义2.特点二、实现方式三、使用场景1.创建复杂对象2.根据不同条件创建对象四、优点1.代码复用2.解耦对象创建和使用3.易于维护五、缺点1.增加代码复杂度2.工厂函数可能变得臃肿六、注意事项1.命名规范2.单一职责原则3.错误......
  • 工厂设计模式
    工厂设计模式工厂设计模式是一种创建型设计模式,它提供了一种创建对象的最佳方式,工厂模式的核心是定义一个创建对象的接口,但是让实现这个接口的类来决定实例化哪一个类。工厂模式将实例化的过程延迟到子类中进行。类型:工厂设计模式有不同的变种,其中包括:简单工厂模式:由一个工厂......
  • 适配器模式
    总结自:《HeadFirst设计模式》适配器的作用是使原本两个不兼容的系统能够兼容起来,比如电源转接口就是一种适配器:假设有一个Duck(鸭子)接口,其中有quack(呱呱叫)和fly方法:publicinterfaceDuck{voidquack();voidfly();}现在如果你还没实现好Duck接口,想先......
  • 微服务设计模式-边车模式(Sidecar Pattern)
    微服务设计模式-边车模式(SidecarPattern)定义边车模式(SidecarPattern)是一种将应用程序的功能分离到一个独立的进程或容器中的设计模式。这个独立的进程或容器被称为边车(Sidecar),它与主应用程序(MainApplication)一起运行,并为其提供额外的功能和服务。边车模式可以看作是一......
  • 从门面模式到 SLF4J 及其 getLogger 方法原理
    基于以下内容总结:从门面模式到Slf4j、10分钟讲清楚JavaSLF4J,Java日志框架的扛把子,从原理到实践写后端接口的时候,先写一个Service接口,这个Service接口的实现中可能会调用多个其他Service或Mapper方法来实现某个业务,对于Controller,只需要传递参数给Service方法就......
  • 使用 NLP 和模式匹配检测、评估和编辑日志中的个人身份信息 - 第 2 部分
    作者:来自Elastic StephenBrown如何使用Elasticsearch、NLP和模式匹配检测、评估和编辑日志中的PII。简介:分布式系统中高熵日志的普遍存在大大增加了PII(PersonallyIdentifiableInformation-个人身份信息)渗入我们日志的风险,这可能导致安全和合规性问题。这篇由两......
  • 研发模式IPD的总结
    1.研发模式介绍:2.角色和指责3.需求分析4.开发串讲对应输出:5.测试策略制定(前置条件:有清晰的需求文档、设计文档和开发文档)对应输出:......
  • 实验3:工厂方法模式
    [实验任务一]:加密算法目前常用的加密算法有DES(DataEncryptionStandard)和IDEA(InternationalDataEncryptionAlgorithm)国际数据加密算法等,请用工厂方法实现加密算法系统。实验要求:1.画出对应的类图;2.提交该系统的代码,该系统务必是一个可以能够直接使用的系统,查阅资料完成相......