简单美食网站模板,wordpress作品链接修改,忘记了wordpress登录密码,茶网站设计素材下载一、背景
最近编码过程中遇到了一个非常奇怪的问题#xff0c;基于单例对象的同步代码块似乎失效了#xff0c;百思不得其姐。
下面给出模拟过程和最终的结论。
二、场景描述和模拟
2.1 现象描述
Database实现单例#xff0c;在 init 方法中使用同步代码块来保证 data不…一、背景
最近编码过程中遇到了一个非常奇怪的问题基于单例对象的同步代码块似乎失效了百思不得其姐。
下面给出模拟过程和最终的结论。
二、场景描述和模拟
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.Database3f99bd52
同步代码块中赋值。类加载器MyClassLoader
第2次加载org.example.classloader.Database19469ea2我们发现我们实际上分别使用了两个类加载器加载同一个类而其中一个类加载器违背了双亲加载机制导致两个类并不相同。
因此原因就找到了我们分别使用了两个类加载器去加载同一个类虽然采用单例的机制实际上并非同一个对象并不能保证同步代码块正确运行。
最终评估第 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.Database3f99bd52
第2次加载org.example.classloader.Database3f99bd52三、相关知识
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如JDBCJNDI等可以使用线程上下文类加载器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);}四、总结
大家在维护一些存在自定义类加载器的框架时一定要特别小心。当发生一些奇奇怪怪的问题时要主动往这个方向考虑。 另外就像我一直说过的“每一个坑都是彻底掌握某个知识的绝佳机会”当我们日常开发中遇到一些坑的时候一定要主动掌握相关原理甚至总结分享。这样对某个知识点的理解和掌握就更加透彻。 创作不易如果本文对你有帮助欢迎点赞、收藏加关注你的支持和鼓励是我创作的最大动力。