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-97return 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;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/822318.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nuxt.js 应用中的 server:devHandler 事件钩子详解

title: Nuxt.js 应用中的 server:devHandler 事件钩子详解 date: 2024/10/26 updated: 2024/10/26 author: cmdragon excerpt: server:devHandler 是 Nuxt.js 中的一个生命周期钩子,它在 Nitro 开发服务器注册开发中间件时被调用。使用这个钩子,开发者可以为开发服务器添加…

HarmonyOS:Node-API实现跨语言交互(2)Node-API支持的数据类型和接口

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤博客园地址:为敢技术(https://www.cnblogs.com/strengthen/ )➤GitHub地址:https://github.com/strengthen➤原文地址:https://www.cnblogs.com/strengthen/p/18502733➤如果链接不是为敢技术的博客园…

【小记】探探学习平台的字体混淆

这篇笔记主要记录了个人对某学习平台字体混淆(防复制)的基本分析和反混淆思路。字体混淆有时候也被用作反爬机制。正在某学习平台做题,想着把题目复制出来和搜索娘深入探讨一下,却发现:嗯?怎么是一坨火星文? 实际上有好几个学习平台都引入了这种字体混淆机制以防止复制,…

KEIL闪退

现象:1.KEIL使用正常,突然出现打开报错,确认后闪退2.打开,在点击project想新建工程时,程序闪退解决办法1.Windows+R 进入注册表2.如图,删除除第一个外的其余Project条目(第一个好像也删不掉)问题解决!

C# 字符串内插

代码 // 字符串内插 Console.Write("字符串内插打印:"); Console.WriteLine($"Hello,{aFriend}");输出若是本人原创文章,请标明:本文来自博客园,作者:huiy_小溪,转载请注明原文链接:https://www.cnblogs.com/huiy/p/18503945。反之,请标明原创作者…

C# 定义一个变量 (2)

代码 // 向控制台打印hello,world! Console.WriteLine("Hello, World!"); // 定义一个变量来进行输出 String aFriend = "Bill"; Console.WriteLine("定义变量后打印:{0}", aFriend);输出若是本人原创文章,请标明:本文来自博客园,作者:huiy…

Free5GC源码研究(7) - NSSF研究

本文研究 Network Slice Selection Function(NSSF)主要实现的功能NSSF的概念 NSSF,也就是网络切片选择功能,负责根据用户请求和网络的配置来选择最合适的网络切片实例(Network Slice Instance, NSI)来服务用户设备。 所谓网络切片,是5G核心网的重要概念,允许运营商在同…

Windows11 24H2系统跳过硬件检测安装

1、下载Windows11系统镜像访问微软官网地址

HarmonyOS:Node-API实现跨语言交互(1)Node-API简介

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤博客园地址:为敢技术(https://www.cnblogs.com/strengthen/ )➤GitHub地址:https://github.com/strengthen➤原文地址:https://www.cnblogs.com/strengthen/p/18503923➤如果链接不是为敢技术的博客园…

如何把一个python列表(有很多个元素)变成一个excel表格的第一列?

大家好,我是Python进阶者。 一、前言 前几天在Python最强王者群有个叫【麦当】的粉丝问了一个关于Python如何把一个python列表(有很多个元素)变成一个excel表格的第一列的问题,这里拿出来给大家分享下,一起学习。二、解决过程 这里给出【dcpeng】和【德善堂小儿推拿-瑜亮老师…

vsftp的三种用户详解

vsfp上有三种用户类型: annoymous 匿名用户 local_user 本地用户 virtual_user 虚拟用户 1、使用匿名用户 不需要认证 主配置文件中配置:anonymous_enable=YES 2、使用本地用户 本地用户,就是linux上的系统用户,满足下面两点就可以使用。 1、用户的bash是/bin/bash 2、主配…