多线程下Class.forName()导致进程终止的原因及解决方法
问题分析与解决方案
首先,咱们先定位核心问题:你的进程在Class.forName(driver)处无声终止,只打印了"Before"却没有"After",也没有异常输出,这大概率是因为静态代码块中抛出了未被捕获的Error而非Exception——你的catch块只处理了IOException和ClassNotFoundException,但像NoClassDefFoundError、UnsatisfiedLinkError这类Error不属于Exception的子类,不会被捕获,JVM遇到未处理的Error会直接终止进程,自然不会打印堆栈信息。
结合你的场景(单线程正常、多线程出问题),再加上代码里的细节,我整理了几个可能的原因和对应的解决办法:
1. 先捕获所有Throwable,定位具体错误
第一步必须先搞清楚到底出了什么错。修改ConnectionUtils的静态代码块catch逻辑,把捕获范围扩大到Throwable,这样不管是Exception还是Error都能被捕获并打印:
static { try { Properties properties = new Properties(); // 这里后面还要改资源加载方式,先解决错误捕获问题 properties.load(new FileReader("src/main/resources/jdbcConnection.properties")); driver = properties.getProperty("driver"); url = properties.getProperty("url"); username = properties.getProperty("username"); password = properties.getProperty("password"); System.out.println("Before"); Class.forName(driver); System.out.println("After"); } catch (Throwable e) { // 打印完整的错误堆栈,包括Error类型 e.printStackTrace(); // 主动抛出RuntimeException,确保进程终止前能看到明确的错误信息 throw new RuntimeException("Failed to initialize ConnectionUtils", e); } }
运行后你就能看到具体的错误原因了,比如驱动类找不到、依赖缺失或者资源加载失败。
2. 修复资源文件加载方式(避免多线程/打包后路径问题)
你用FileReader读取资源文件的方式有问题:src/main/resources在IDE里是本地路径,但项目打包成JAR后,这个路径就不存在了,而且多线程环境下类加载路径可能有差异,导致单线程能读到,多线程读不到(或者加载时机不对)。
改成用类加载器读取资源流,这才是标准的资源加载方式:
// 替换原来的FileReader加载逻辑 properties.load(ConnectionUtils.class.getClassLoader().getResourceAsStream("jdbcConnection.properties"));
这样不管是IDE运行还是JAR包运行,都能正确读取到properties文件。
3. 检查JDBC驱动的正确性与依赖
如果捕获到NoClassDefFoundError,那就是驱动类没找到:
- 确认
jdbcConnection.properties里的driver值正确:比如MySQL 8.0+要用com.mysql.cj.jdbc.Driver,而不是旧的com.mysql.jdbc.Driver;Oracle的话是oracle.jdbc.driver.OracleDriver - 确认项目依赖里已经包含了对应的JDBC驱动包:比如Maven项目要在pom.xml里添加mysql-connector-java的依赖,版本要和数据库版本匹配
- 如果是Java模块化项目(JDK 9+),需要在
module-info.java里声明对驱动模块的依赖,比如requires mysql.connector.java;
4. 修复ConnectionUtils的线程安全设计
你的ConnectionUtils里同时有static Connection connection和ThreadLocal<Connection> t,这是矛盾的设计!静态Connection是全局共享的,多线程下会导致连接混乱,而ThreadLocal是为了每个线程独占一个连接,两者不能共存。
正确的做法是只保留ThreadLocal来管理连接,同时确保每个线程使用完连接后关闭并移除ThreadLocal中的引用,避免内存泄漏:
public class ConnectionUtils { static private String url; static private String driver; static private String username; static private String password; private static ThreadLocal<Connection> t = new ThreadLocal<>(); static { try { Properties properties = new Properties(); properties.load(ConnectionUtils.class.getClassLoader().getResourceAsStream("jdbcConnection.properties")); driver = properties.getProperty("driver"); url = properties.getProperty("url"); username = properties.getProperty("username"); password = properties.getProperty("password"); Class.forName(driver); } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException("Failed to initialize ConnectionUtils", e); } } // 获取连接:从ThreadLocal拿,没有则创建 public static Connection getConnection() throws SQLException { Connection conn = t.get(); if (conn == null || conn.isClosed()) { conn = DriverManager.getConnection(url, username, password); t.set(conn); } return conn; } // 关闭连接:从ThreadLocal移除并关闭 public static void closeConnection() { Connection conn = t.get(); if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } finally { t.remove(); // 必须移除,避免ThreadLocal内存泄漏 } } } }
5. 完善事务处理的异常回滚逻辑
你的testTransaction()方法在catch块里只打印异常,没有回滚事务,这会导致部分执行的SQL留在数据库连接中,可能引发后续问题。修改catch块添加回滚:
private void testTransaction() { Connection connection = null; try { connection = ConnectionUtils.getConnection(); connection.setAutoCommit(false); // 执行SQL Product product = new Product(1, 4000d); productService.updateProduct(connection, product); Log log = new Log(true, "None"); logService.insertLog(connection, log); // 提交事务 connection.commit(); } catch (Exception e) { e.printStackTrace(); // 异常时回滚事务 if (connection != null) { try { connection.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } } finally { ConnectionUtils.closeConnection(); } }
按这个步骤排查和修改,应该就能解决进程无声终止的问题了。
内容的提问来源于stack exchange,提问作者sh-zh-7

