什么?同步代码块失效了?-- 自定义类加载器引起的问题

news2025/7/12 15:14:38

一、背景

最近编码过程中遇到了一个非常奇怪的问题,基于单例对象的同步代码块似乎失效了,百思不得其姐。

下面给出模拟过程和最终的结论。

二、场景描述和模拟

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 {

    @Override
    public String getName() {
        return "MyClassLoader";
    }

    // 类文件的根目录
    private String rootDir;

    // 构造方法,传入类文件的根目录
    public MyClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }


    // 重写 loadClass 方法,打破双亲加载机制
    @Override
    protected 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 方法,实现自己的类加载逻辑
    @Override
    protected 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

我们发现,我们实际上分别使用了两个类加载器加载同一个类,而其中一个类加载器违背了双亲加载机制,导致两个类并不相同。
Xnip2023-03-07_23-52-24.png

因此,原因就找到了,我们分别使用了两个类加载器去加载同一个类,虽然采用单例的机制,实际上并非同一个对象,并不能保证同步代码块正确运行。

最终评估第 2 部分不需要让自定义类加载器来加载,将该部分逻辑从自定义类加载器的条件中移除,问题就解决了。

假如上面的例子我们修改父类优先加载:

    @Override
    protected 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 部分如下:

    @Override
    protected 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 部分如下:

    @Override
    protected 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);
    }

四、总结

大家在维护一些存在自定义类加载器的框架时一定要特别小心。当发生一些奇奇怪怪的问题时,要主动往这个方向考虑。
另外就像我一直说过的“每一个坑都是彻底掌握某个知识的绝佳机会”,当我们日常开发中遇到一些坑的时候,一定要主动掌握相关原理,甚至总结分享。这样对某个知识点的理解和掌握就更加透彻。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Revit中图纸要怎么布局呢?

1、明确图纸布局原则。 2、在图纸上锁定视图的位置 在图纸上放置视图(或明细表)并根据需要对其定位后&#xff0c;可以将其锁定到位&#xff0c;这样就不会在无意中移动它。如果想要解锁视图&#xff0c;单击锁定图标即可&#xff0c;如图1所示。 3、在图纸上旋转视图 可以在图…

imx6 usb增强信号强度

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

xcode14安装swift package设置github账户token

这里写目录标题登录github账户,复制token打开xcode添加github账户选择swift package登录github账户,复制token 登录github点击上面菜单自己的头像,settings->Developer settings->Personal access tokens->Tokens (classic)->Generate new token (classic) Note名…

Spring 响应式编程-读书笔记

序言 大家好&#xff0c;我是比特桃。本文为《Spring 响应式编程》的读书笔记&#xff0c;响应式技术栈可以创建极其高效、易于获取且具有回弹性的端点&#xff0c;同时响应式可以容忍网络延迟&#xff0c;并以影响较小的方式处理故障。响应式微服务还可以隔离慢速事务并加速速…

判断推理之图形推理

考点一动态位置变化&#xff08;一&#xff09;平移1.特征&#xff1a;图形在平面上的移动&#xff0c;图形本身的大小和形状不发生改变。2.方向&#xff1a;直线&#xff08;上下、左右、斜对角线&#xff09;&#xff0c;绕圈&#xff08;顺时针、逆时针&#xff09;3.距离&a…

聚观早报 |王兴宣布美团网约车业务调整;软银Arm被曝4月申请上市

今日要闻&#xff1a;王兴宣布美团网约车业务调整&#xff1b;钉钉收购协同办公厂商「我来 wolai」&#xff1b;软银旗下Arm被曝4月申请上市&#xff1b;小米汽车完成冬测&#xff1b;淘特负责人否认将被合并到淘宝 王兴宣布美团网约车业务调整 美团创始人王兴发布内部信&#…

蓝桥杯入门即劝退(二十六)组合问题(回溯算法)

-----持续更新Spring入门系列文章----- 如果你也喜欢Java和算法&#xff0c;欢迎订阅专栏共同学习交流&#xff01; 你的点赞、关注、评论、是我创作的动力&#xff01; -------希望我的文章对你有所帮助-------- 专栏&#xff1a;蓝桥杯系列 一、题目描述 给定两个整数 n …

css:使用filter和backdrop-filter实现高斯模糊效果

背景 今天接到一个需求是&#xff0c;使用高斯模糊的效果对一个页面进行模糊处理&#xff0c;正好借这个机会来整理一下 css3 中高斯模糊的两个 API API介绍 filter 说明&#xff1a; 该 API 是一个过滤器&#xff0c;不仅能实现高斯模糊&#xff0c;还有很多比如颜色偏移、…

【Linux】网络基础(1)

前言 相信没有网络就没有现在丰富的世界。本篇笔记记录我在Linux系统下学习网络基础部分知识&#xff0c;从关于网络的各种概念和关系开始讲起&#xff0c;逐步架构起对网络的认识&#xff0c;对网络编程相关的认知。 我的上一篇Linux文章呀~ 【Linux】网络套接字编程_柒海啦的…

利用 socket.io 实现前后端实时交互

官网地址&#xff1a;Socket.IO 项目结构&#xff1a; 服务端&#xff08;node&#xff09;代码&#xff1a; 首先安装 express&#xff0c;socket.io npm i express socket.io -S/server/index.js // 官网用例&#xff1a;https://socket.io/zh-CN/docs/v4/server-initial…

数据库-基础篇-6-多表查询(内连接、外连接、自连接)

一、多表关系 1.概述&#xff1a;项目开发中&#xff0c;在进行数据库表结构设计时&#xff0c;会根据业务需求及业务模块之间的关系&#xff0c;分析并设计表结构&#xff0c;由于业务之间相互关联&#xff0c;所以各个表结构也存在着各种联系&#xff0c;基本上分为三种&…

idea中 使用git实现远程仓库master分支和dev分支互相合并

一 新建dev分支 1.在右下角当前分支下&#xff0c;选择创建分支选项 2.给分支起名字 3.创建后&#xff0c;自动切换成新分支 4.将dev分支内容提交到远程dev仓库 进行add&#xff0c;commit&#xff0c;pull&#xff0c;push操作。 1commit操作 2.进行pull操作&#xff0c;远…

浅析CSRF跨域读取型漏洞之CORS

目录 前提知识 CORS介绍 跨域访问的一些场景 跨域请求方式 漏洞原理 非简单请求的预检过程 安全隐患 漏洞复现 挖掘技巧 防御 前提知识 CORS介绍 H5提供的一种机制&#xff0c;WEB应用程序可以通过在HTTP增加字段来告诉浏览器&#xff0c;哪些不同来源的服务器是有权…

7天收割10个offer,软件测试面试题 (项目经验问题+回答)(超级全细)

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 1、简单介绍下最近做…

Android Vsync原理简析

屏幕渲染原理"现代计算机之父"冯诺依曼提出了计算机的体系结构: 计算机由运算器&#xff0c;存储器&#xff0c;控制器&#xff0c;输入设备和输出设备构成&#xff0c;每部分各司其职&#xff0c;它们之间通过控制信号进行交互。计算机发展到现在&#xff0c;已经出…

E90-DTU系列无线数传电台网关与节点4gDTU通信教程

以E90-DTU(400SL22-ETH)与E90-DTU(400SL22)为例实现网关与节点进行数据交换&#xff0c;其它频段或功率的网关/节点&#xff0c;其操作方式与该视频教程一致。 第一步【安装网关】 为网关安装天线电源模块。 第二步【调试网关】&#xff08;根据连接方式不同&#xff0c;配置方…

【java】Java 重写(Override)与重载(Overload)

文章目录重写(Override)方法的重写规则Super 关键字的使用重载(Overload)重载规则实例重写与重载之间的区别总结重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变&#xff0c;核心重写&#xff01; 重写的好处在于…

城市通电(prim算法)

acwing3728 蓝桥杯集训每日一题 平面上遍布着 n 座城市&#xff0c;编号 1∼n。 第 i 座城市的位置坐标为 (xi,yi) 不同城市的位置有可能重合。 现在要通过建立发电站和搭建电线的方式给每座城市都通电。 一个城市如果建有发电站&#xff0c;或者通过电线直接或间接的与建…

重温数据结构与算法之深度优先搜索

文章目录前言一、实现1.1 递归实现1.2 栈实现1.3 两者区别二、LeetCode 实战2.1 二叉树的前序遍历2.2 岛屿数量2.3 统计封闭岛屿的数目2.4 从先序遍历还原二叉树参考前言 深度优先搜索&#xff08;Depth First Search&#xff0c;DFS&#xff09;是一种遍历或搜索树或图数据结…

数据结构(七)优先级队列——堆

一、优先级队列概念队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场景下&#xff0c;使用队列显然不合适&#xff0c;比如&#xff1…