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

java——了解反射
目录 什么是反射? 反射如何获取类信息? 小结: 什么是反射? 反射是用代码分析类信息的能力 类中有哪些信息:方法、对象、构造器、全局变量、父类、接口等.... 反射如何获取类信息? 三种方式 1.通过对象…...

The Sandbox 中的独特体验——《奥米加》
在过去几年间,The Sandbox 游戏变得越来越受欢迎。因为我们为玩家提供了在虚拟世界中探索、创造和游戏的自由,没有线性游戏的限制。DeQuest 工作室创作的《奥米加》也正是如此,绝对是一个前所未有的体验! 先了解一下《奥米加》的故…...

76 Python写入csv文件时出现空行_newline参数解决
76 Python写入csv文件时出现空行_newline参数解决 文章目录76 Python写入csv文件时出现空行_newline参数解决1. 准备工作2. with open 语句没有newline参数3. with open 语句有newline参数4. 总结1. 准备工作 在电脑D盘新建一个【76】文件夹。 用VScode编辑器打开【76】文件夹…...

高等数学——定积分和不定积分
文章目录不定积分概念几何意义性质不定积分的基本公式三种主要积分法三类常见可积函数积分定积分概念几何意义性质积分上限的函数定积分的计算几何应用反常积分无穷区间的反常积分无界函数的反常积分不定积分 不定积分是导数的逆运算。 概念 原函数:设f(x)f(x)f(…...

imx6 usb增强信号强度
USB信号 参考:官方文档 USB信号完整性取决于许多因素,如电路设计、PCB布局、堆叠和阻抗。每个产品可能彼此不同,因此客户需要微调参数,以获得最佳的信号质量。 测试板已经路由出两个USB端口:一个OTG1,一个主机。每个端…...

深入理解性能压测工具原理
如果没有性能测试工具如何通过手工实现 如果没有性能测试工具,通过手工进行性能测试,这是一个值得我们思考的问题。这时候需要一个协调员发送指令,一个操作员进行操作,对系统施加压力,多个操作员代表着多个用户进行并…...

Java的概述和运行方式
目录 一.Java是什么? 1.1Java的目前状况和学习需求 1.2Java的平台分类和特点 二.Java程序的运行方式 2.1 Java的程序结构 2.2 JDK、JRE、JVM的关系 2.3 Java运行详情 总结 😽个人主页:tq02的博客_CSDN博客-领域博主 🌈理想…...

【C语言】每日刷题 —— 牛客
前言 大家好,今天带来一篇新的专栏 c_牛客,不出意外的话每天更新十道题,难度也是从易到难,自己复习的同时也希望能帮助到大家,题目答案会根据我所学到的知识提供最优解。 🏡个人主页:悲伤的猪大…...

JavaEE课程实践-Servlet的部署(tomcat服务器)
目录 Servlet简述 tomcat服务器的安装和运行 Servlet的部署 部署具体步骤 一、创建maven工程 二、创建Servlet类 三、导入相应jar包 四、编写Servlet代码 五、运行maven项目,启动tomcat服务器 六、测试访问是否成功。 Servlet简述 Servlet 是 Java EE 技术…...

Java 中的拆箱和装箱
在 Java 中,每个基本数据类型都对应了一个包装类型,比如:int 的包装类型是 Integer,double 的包装类型是 Double…那么,基本数据类型和包装类型有什么区别呢? 大概有以下几点区别: 成员变量的…...