什么?同步代码块失效了?-- 自定义类加载器引起的问题
一、背景
最近编码过程中遇到了一个非常奇怪的问题,基于单例对象的同步代码块似乎失效了,百思不得其姐。
下面给出模拟过程和最终的结论。
二、场景描述和模拟
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数据传输格式,…...

业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 
江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...