Java类循环依赖导致类加载阻塞问题及解决方案咨询
问题根因
你遇到的是JVM类初始化阶段的循环依赖死锁,JVM规范要求类初始化过程是线程安全的:同一个类的初始化只会被一个线程持有初始化锁,其他访问该类的线程会被阻塞直到初始化完成。
你的代码形成了循环依赖链:Misc初始化 → 调用GetDBData.execute() → 实例化GetDBData → 读取Constants.mysql → 触发Constants初始化 → 读取Misc.sql。当两个线程刚好分别触发Misc和Constants的初始化时,就会出现Misc等Constants初始化完成、Constants等Misc初始化完成的死锁情况。你遇到的SQL报错也是因为死锁时Misc.sql还未完成赋值,Constants.mysql拿到空值,拼接后的SQL语句缺失必要内容导致的。
方案解答
1. 单例改造方案
改造成本确实过高,完全不需要采用。
2. 解耦Constants和Misc
这是根治方案,如果暂时无法处理所有依赖,可以先采用临时方案兜底。
3. JSP调用迁移到Action的可行性
不能解决问题。JSP和Struts Action本质没有线程模型差异:JSP会被编译成Servlet运行,和Action一样都是Tomcat线程池调度的HTTP请求处理线程。只要并发请求分别触发Misc和Constants的初始化,死锁仍然会发生。
4. 低改造成本方案
以下方案都不需要修改原有业务调用逻辑,改造成本极低:
- 方案1:启动时主动预热类加载
新增一个ServletContextListener,在应用启动阶段就主动触发Misc和Constants的类初始化,用户请求进入时所有相关类已经初始化完成,完全避免运行时并发加载的死锁风险。只需要新增一个监听器类,在web.xml中注册即可,无需修改任何业务代码。
代码示例:public class ClassInitListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { // 主动触发类初始化 try { Class.forName("Misc"); Class.forName("Constants"); } catch (ClassNotFoundException e) { throw new RuntimeException("初始化核心类失败", e); } } } - 方案2:修改Misc为懒加载逻辑
把dbData的初始化逻辑从静态代码块移到getDbData()方法中,用双重检查锁实现懒加载,静态代码块仅负责sql常量的赋值,避免类初始化阶段触发依赖链。原有10000多处Misc.getDbData()调用完全不需要修改:public class Misc { public static final String sql = "select"; private static volatile String dbData; static { System.out.println("static started"); // 仅初始化常量,不触发依赖调用 System.out.println("static ended"); } public static String getDbData() { System.out.println("getDbData"); if (dbData == null) { synchronized (Misc.class) { if (dbData == null) { dbData = GetDBData.execute(); } } } return dbData; } } - 方案3:修改Constants常量加载逻辑
把Constants.mysql从静态常量改为静态方法返回,避免Constants初始化阶段直接读取Misc.sql,切断类初始化阶段的循环依赖:
只需要把原有public class Constants { public static String getMysql() { return Misc.sql; } }Constants.mysql的调用改为Constants.getMysql()即可,修改范围仅局限在GetDBData类中。
内容的提问来源于stack exchange,提问作者Lucian L.
相关产品推荐
相关产品推荐

