什么?同步代码块失效了?-- 自定义类加载器引起的问题
一、背景
最近编码过程中遇到了一个非常奇怪的问题,基于单例对象的同步代码块似乎失效了,百思不得其姐。
下面给出模拟过程和最终的结论。
二、场景描述和模拟
2.1 现象描述
Database
实现单例,在 init 方法中使用同步代码块来保证 data
不会被重复赋值,因此打印语句不应该重复打印。
public class Database {private static final Database dbObject = new Database();private volatile String data;private Database() {}public static Database getInstance() {return dbObject;}public void init() {synchronized (this) {if (data == null) {data = "test";System.out.println("同步代码块中赋值。" );}}}
}
在构造 MyClass
的时候会自动获取 Database
单例,并执行 init
方法。
public class MyClass {private Database database;public MyClass() {database = Database.getInstance();database.init();}public Database getDatabase() {return database;}
}
在业务代码中会自动创建 MyClass
对象,因此会多次获取 Database
单例并执行 init
方法。
由于是单例 synchronized(this)
就可以保证 init 中的打印语句不会多次执行,但是从日志看最终执行了两次。
2.2 场景模拟
最终发现,实际上项目中自定义了类加载器,导致的。
自定义该类加载器的目的是为了避免类冲突,保证该框架使用的某个 Jar 包固定在特定版本,又不影响用户使用其他版本。
package org.example.classloader;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;public class MyClassLoader extends ClassLoader {@Overridepublic String getName() {return "MyClassLoader";}// 类文件的根目录private String rootDir;// 构造方法,传入类文件的根目录public MyClassLoader(String rootDir) {this.rootDir = rootDir;}// 重写 loadClass 方法,打破双亲加载机制@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 自己先加载Class<?> clazz = null;try {clazz = findClass(name);} catch (ClassNotFoundException e) {// 自己加载器加载失败,不做处理}// 如果自己加载器加载成功,直接返回if (clazz != null) {return clazz;}// 如果自己加载器加载失败,调用父加载器的 findClass 方法加载类return super.loadClass(name, resolve);}// 重写 findClass 方法,实现自己的类加载逻辑@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 根据类名获取类文件的路径String classPath = rootDir + File.separator + name.replace(".", File.separator) + ".class";// 读取类文件的字节码byte[] classBytes = getClassBytes(classPath);// 如果字节码为空,抛出异常if (classBytes == null) {throw new ClassNotFoundException("Cannot find class: " + name);}// 调用 defineClass 方法将字节码转换为 Class 对象return defineClass(name, classBytes, 0, classBytes.length);}// 读取类文件的字节码private byte[] getClassBytes(String classPath) {// 创建文件对象File file = new File(classPath);// 如果文件不存在,返回空if (!file.exists()) {return null;}// 创建字节数组,长度为文件大小byte[] bytes = new byte[(int) file.length()];// 创建文件输入流try (FileInputStream fis = new FileInputStream(file)) {// 读取文件内容到字节数组fis.read(bytes);} catch (IOException e) {// 发生异常,返回空return null;}// 返回字节数组return bytes;}
}
模拟代码如下:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class ClassLoaderDemo {public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {// 第一次执行MyClass myClass = new MyClass();System.out.println("第1次加载" + myClass.getDatabase());// 第二次执行MyClassLoader myClassLoader = new MyClassLoader("~/IdeaProjects/test/target/classes/");Class<?> myClazz = myClassLoader.loadClass("org.example.classloader.MyClass", false);Object obj = myClazz.newInstance();Method getDatabase = myClazz.getMethod("getDatabase");System.out.println("第2次加载" + getDatabase.invoke(obj));}
}
为了更好地排查问题,我们在打印语句中打印类加载器:
public class Database {private static final Database dbObject = new Database();private volatile String data;private Database() {}public static Database getInstance() {return dbObject;}public void init() {synchronized (this) {if (this.data == null) {data = "test";System.out.println("同步代码块中赋值。类加载器" + this.getClass().getClassLoader().getName());}}}
}
实际没有那么明显,比如第一个MyClass
部分在 Spring 初始化方法中自动创建。第二个 MyClass
则是在运行时从 jar 包中动态加载时自动创建的。
控制台输出:
同步代码块中赋值。类加载器app
第1次加载org.example.classloader.Database@3f99bd52
同步代码块中赋值。类加载器MyClassLoader
第2次加载org.example.classloader.Database@19469ea2
我们发现,我们实际上分别使用了两个类加载器加载同一个类,而其中一个类加载器违背了双亲加载机制,导致两个类并不相同。
因此,原因就找到了,我们分别使用了两个类加载器去加载同一个类,虽然采用单例的机制,实际上并非同一个对象,并不能保证同步代码块正确运行。
最终评估第 2 部分不需要让自定义类加载器来加载,将该部分逻辑从自定义类加载器的条件中移除,问题就解决了。
假如上面的例子我们修改父类优先加载:
@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 先委托父类加载器加载类Class<?> clazz = null;try {clazz = super.loadClass(name, resolve);} catch (ClassNotFoundException e) {// 父类加载器加载失败,不做处理}// 如果父类加载器加载成功,直接返回if (clazz != null) {return clazz;}// 如果父类加载器加载失败,调用自己的 findClass 方法加载类return findClass(name);}
发现单例“生效”, init 也不会打印两次。
同步代码块中赋值。类加载器app
第1次加载org.example.classloader.Database@3f99bd52
第2次加载org.example.classloader.Database@3f99bd52
三、相关知识
3.1 类加载机制
3.1.1 双亲加载机制
Java类加载器有以下几种:
- 引导类加载器(Bootstrap ClassLoader):它是用原生代码实现的,不继承自java.lang.ClassLoader,负责加载Java的核心库,如java.lang.*,以及jre/lib文件夹下的jar包和class文件。
- 扩展类加载器(ExtClassLoader):它继承自java.lang.ClassLoader,负责加载Java的扩展库,如jre/lib/ext文件夹下的jar包和class文件。
- 应用类加载器(AppClassLoader):它也继承自java.lang.ClassLoader,负责加载用户的类路径(classpath)下的jar包和class文件。
- 自定义类加载器(User-Defined ClassLoader):它们是由开发人员自定义的类加载器,继承自java.lang.ClassLoader,可以实现一些特殊的需求,如动态加载,热部署,加密解密等。
这些类加载器之间的关系是一个父子层次结构,除了引导类加载器外,每个类加载器都有一个父类加载器。当一个类加载器收到一个类加载请求时,它会先委托给它的父类加载器,如果父类加载器无法加载,它才会尝试自己加载。这样可以保证核心类库的优先加载,避免被恶意替换。
本文所列的场景就是违背双亲加载机制的一个案例。
3.1.2 双亲类加载机制的目的
- 可以避免类的重复加载,确保一个类的全局唯一性。因为双亲委派机制是向上委托加载的,所以当父类加载器已经加载了该类时,就没有必要子类加载器再加载一次。
- 可以保护程序安全,防止核心API被随意篡改。因为 Java 的核心API都是通过引导类加载器进行加载的,如果别人通过定义同样路径的类比如
java.lang.Integer
,类加载器通过向上委派,会发现引导类加载器已经加载了jdk 的Integer
类,而不会加载自定义的Integer
类。这样就阻止了对核心API的恶意修改。
3.1.3 遵循双亲加载机制的自定义类加载器的示例
如果想自定义遵循双亲加载机制的类加载器,需要以下三个步骤:
- 继承
java.lang.ClassLoader
类,实现一个自己的类加载器。 - 重写
findClass
方法,实现自己的类查找逻辑。例如,从指定的路径或者网络上加载类的字节码,然后调用defineClass
方法将字节码转换为 Class 对象。 - 重写
loadClass
方法,遵循类加载的顺序或方式。例如,优先使用父加载器加载,如果加载不到,再交使用本类加载器加载。
具体代码,参考上文中的 MyClassLoader
loadClass 部分如下:
@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 先委托父类加载器加载类Class<?> clazz = null;try {clazz = super.loadClass(name, resolve);} catch (ClassNotFoundException e) {// 父类加载器加载失败,不做处理}// 如果父类加载器加载成功,直接返回if (clazz != null) {return clazz;}// 如果父类加载器加载失败,调用自己的 findClass 方法加载类return findClass(name);}
3.2 违背双亲加载机制
3.2.1 违背双亲加载机制的场景
违背双亲加载机制的情况有以下几种:
- 为了避免类冲突,每个web应用项目中都有自己的类加载器,可以加载自己的类库,而不受其他项目的影响。例如,
Tomcat
中的WebAppClassLoader
就会优先加载自己的类,如果加载不到,再交给父类加载器走双亲委派机制。 - 为了实现一些特殊的需求,如动态加载,热部署,加密解密等,可以自定义类加载器,覆盖
loadClass
方法,改变类加载的顺序或方式。例如,OSGi 框架就是通过自定义类加载器,实现了模块化和动态更新的功能。 - 为了支持一些服务提供者接口(SPI),如JDBC,JNDI等,可以使用线程上下文类加载器(Thread Context ClassLoader),让启动类加载器加载的类可以使用应用类加载器加载的类。例如,
java.sql.DriverManager
类是由启动类加载器加载的,但是它需要加载不同厂商提供的java.sql.Driver
接口的实现类,这些实现类是由应用类加载器加载的,所以DriverManager
类就使用了线程上下文类加载器,打破了双亲委派机制。
本文的例子的场景就是为了避免类冲突而自定义类加载器。
3.2.2 违背双亲加载机制的类加载器
如果想自定义违背双亲加载机制的类加载器,需要以下三个步骤:
- 继承
java.lang.ClassLoader
类,实现一个自己的类加载器。 - 重写
findClass
方法,实现自己的类查找逻辑。例如,从指定的路径或者网络上加载类的字节码,然后调用defineClass
方法将字节码转换为 Class 对象。 - 重写
loadClass
方法,改变类加载的顺序或方式。例如,优先加载自己的类,如果加载不到,再交给父类加载器走双亲委派机制。
具体代码,参考上文中的 MyClassLoader
loadClass 部分如下:
@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 自己先加载Class<?> clazz = null;try {clazz = findClass(name);} catch (ClassNotFoundException e) {// 自己加载器加载失败,不做处理}// 如果自己加载器加载成功,直接返回if (clazz != null) {return clazz;}// 如果自己加载器加载失败,调用父加载器的 findClass 方法加载类return super.loadClass(name, resolve);}
四、总结
大家在维护一些存在自定义类加载器的框架时一定要特别小心。当发生一些奇奇怪怪的问题时,要主动往这个方向考虑。
另外就像我一直说过的“每一个坑都是彻底掌握某个知识的绝佳机会”,当我们日常开发中遇到一些坑的时候,一定要主动掌握相关原理,甚至总结分享。这样对某个知识点的理解和掌握就更加透彻。
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
相关文章:

什么?同步代码块失效了?-- 自定义类加载器引起的问题
一、背景 最近编码过程中遇到了一个非常奇怪的问题,基于单例对象的同步代码块似乎失效了,百思不得其姐。 下面给出模拟过程和最终的结论。 二、场景描述和模拟 2.1 现象描述 Database实现单例,在 init 方法中使用同步代码块来保证 data不…...

CHAPTER 4 文件共享 - Samba
文件共享 - Samba1 Samba1.1 Samba的软件架构1.2 搭建Samba服务器1.3 samba用户管理1. 添加用户2. 修改用户密码3. 删除用户和密码4. 查看samba用户列表5. 查看samba服务器状态1.4 samba共享设置(配置文件详解)1.5 访问共享目录1. windows访问2. linux客…...

深入分析@Configuration源码
文章目录一、源码时序图1. 注册ConfigurationClassPostProcessor流程源码时序图2. 注册ConfigurationAnnotationConfig流程源码时序图3. 实例化流程源码时序图二、源码解析1. 注册ConfigurationClassPostProcessor流程源码解析(1)运行案例程序启动类Conf…...
Unity 代码优化 内存管理优化
项目遇到了卡顿的情况 仔细检查了代码没检查出有误的地方 仔细的总结了一下可以优化的东西 解决了卡顿 记录一下 1 协程 项目之前写的关于倒计时之类的东西 都是开了个协程 虽然协程是消耗很小的线程 , 可是还是有额外消耗 而且 有很多用携程来检测销毁预制体的操作 也都放到U…...

设计模式~门面(外观)模式(Facade)-08
目录 (1)优点 (2)缺点 (3)使用场景 (4)注意事项: (5)应用实例: (6)源码中的经典应用 代码 外观模式&am…...

C++面向对象编程之一:封装
C面向对象编程三大特性为:封装,继承,多态。C认为万事万物皆为对象,对象有属性和行为。比如:游戏里的地图场景可以看作是长方形对象,属性场景id,有长,有宽,可能有NPC&…...

IDEA插件系列(3):Maven Helper插件
一、引言在写Java代码的时候,我们可能会出现Jar包的冲突的问题,这时候就需要我们去解决依赖冲突了,而解决依赖冲突就需要先找到是那些依赖发生了冲突,当项目比较小的时候,还比较依靠IEDA的【Diagrams】查看依赖关系&am…...

SAP 更改物料基本计量单位
前言部分 在SAP中物料创建后,一旦发生业务,其基本计量单位便很难修改。由于单位无法满足业务要求,往往会要求新建一个物料替代旧物料。这时候除了要将旧物料上所有的未清业务删除外,还需要替换工艺与BOM中的旧物料。特别是当出现旧…...

蓝桥web基础知识学习
HTMLCSS 知识点重要指数HTML 基础标签🌟🌟🌟🌟🌟HTML5 新特性🌟🌟🌟🌟🌟HTML5 本地存储🌟🌟🌟🌟CSS 基础语法…...

Python+ChatGPT制作一个AI实用百宝箱
目录一、注册OpenAI二、搭建网站及其框架三、AI聊天机器人四、AI绘画机器人ChatGPT 最近在互联网掀起了一阵热潮,其高度智能化的功能能够给我们现实生活带来诸多的便利,可以帮助你写文章、写报告、写周报、做表格、做策划甚至还会写代码。只要与文字相关…...
Python中格式化字符串输出的4种方式
Python格式化字符串的4中方式 一、%号 二、str.format(args) 三、f-Strings 四、标准库模板 五、总结四种方式的应用场景’ 一、%号占位符 这是一种引入最早的一种,也是比较容易理解的一种方式.使用方式为: 1、格式化字符串中变化的部分使用占位符 2、…...
C#基础教程15 枚举与类
文章目录 C# 枚举(Enum)声明 enum 变量C# 类(Class)类的定义成员函数和封装C# 中的构造函数关键字 staticC# 枚举(Enum) 枚举是一组命名整型常量。枚举类型是使用 enum 关键字声明的。 C# 枚举是值类型。换句话说,枚举包含自己的值,且不能继承或传递继承。 声明 enum 变…...

三步 让你的 vscode 自动编译ts文件
三步让你的 vscode 自动编译ts文件 TypeScript环境安装与如何在vscode实现自动编译ts文件? 文章目录三步让你的 vscode 自动编译ts文件前提条件环境安装自动编译运行监视任务时报错?前提条件 安装 node 环境 环境安装 tsc 作用:负责将ts 代码 转为 浏…...

STM32程序下载和启动方式
目录1 BOOT引脚配置和下载说明2 关于串口下载方式3 关于一按复位就跑代码4 关于下载调试速度5 关于三种启动方式5.1 FLASH启动5.2 系统存储器器启动5.3 SRAM启动6 关于程序的三种下载方式1 BOOT引脚配置和下载说明 BOOT0BOOT1程序运行ST-Link下载串口下载启动说明xx无0x√√用…...
基础01-ajax fetch axios 的区别
ajax fetch axios 的区别 题目 ajax fetch axios 的区别 分析 三者根本没有可比性,不要被题目搞混了。要说出他们的本质 传统 ajax AJAX (几个单词首字母,按规范应该大写) - Asynchronous JavaScript and XML(异…...

Android Execution failed for task ‘:app:mergeDebugJavaResource
错误提示 FAILURE: Build failed with an exception.* What went wrong: Execution failed for task :app:mergeDebugJavaResource. > A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction> 2 files found with path k…...

spring事物源码分析
今天的任务是剖析源码,看看Spring 是怎么运行事务的,并且是基于当前最流行的SpringBoot。还有,我们之前剖析Mybatis 的时候,也知道,Mybatis 也有事务,那么,他俩融合之后,事务是交给谁…...

炫龙游戏本Win10系统总是蓝屏崩溃怎么办?
炫龙游戏本Win10系统总是蓝屏崩溃怎么办?有用户使用的炫龙游戏本最近总是在运行的过程中出现自动蓝屏的情况,有的时候自己还在操作电脑,而屏幕却蓝屏了,导致自己的工作被中断了。那么这个情况要怎么去进行修复呢?来看看…...

华为OD机试题,用 Java 解【数字加减游戏】问题
华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典使用说明 参加华为od机试,一定要注意不…...
C++ 手写一个高性能json生成与解析器
文章目录 前言一、了解json格式二、统一数据类型:TJson三、解析json数据四、输出json数据五、实现便利的修改与访问六、性能优化七、源码下载与解析前言 由于C++标准库中并不存在解析json数据库,但json格式又非常的常见 如今绝大部分网络数据传输都采用的json数据传输格式,…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...

零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...

用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...

Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...

Unity UGUI Button事件流程
场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...
Leetcode33( 搜索旋转排序数组)
题目表述 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], …, nums[n-1], nums[0], nu…...

VisualXML全新升级 | 新增数据库编辑功能
VisualXML是一个功能强大的网络总线设计工具,专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑(如DBC、LDF、ARXML、HEX等),并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...

医疗AI模型可解释性编程研究:基于SHAP、LIME与Anchor
1 医疗树模型与可解释人工智能基础 医疗领域的人工智能应用正迅速从理论研究转向临床实践,在这一过程中,模型可解释性已成为确保AI系统被医疗专业人员接受和信任的关键因素。基于树模型的集成算法(如RandomForest、XGBoost、LightGBM)因其卓越的预测性能和相对良好的解释性…...
前端工具库lodash与lodash-es区别详解
lodash 和 lodash-es 是同一工具库的两个不同版本,核心功能完全一致,主要区别在于模块化格式和优化方式,适合不同的开发环境。以下是详细对比: 1. 模块化格式 lodash 使用 CommonJS 模块格式(require/module.exports&a…...