You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java多线程并发Class.forName加载JDBC驱动死锁解决方案

问题根因

这个死锁本质是多线程并发加载JDBC驱动时触发的锁顺序倒置,和你代码里给SqliteConn加的synchronized修饰没有关系:

  • JDK8及更早版本中,类加载器加载类的loadClass方法是同步方法,整个类加载过程会持有当前类加载器的对象锁
  • 所有JDBC驱动被Class.forName加载完成后,会在自身静态初始化块中调用DriverManager.registerDriver完成注册,这个注册操作必须持有DriverManager类的内置锁
  • DriverManager第一次被调用触发初始化时,会先持有自身的内置锁,再扫描classpath下所有JDBC驱动配置,反向调用类加载器加载这些驱动类,这一步又需要获取类加载器的锁
  • 多线程同时启动时就会出现锁循环等待:加载ClickHouse驱动的线程先拿到类加载器锁,等待DriverManager的锁完成驱动注册;加载SQLite驱动的线程先触发DriverManager初始化拿到它的锁,等待类加载器锁加载SQLite驱动,两边互相持有对方需要的锁不释放,就形成了死锁。

你之前加Thread.sleep的方案只是靠时间差错开了锁竞争的时机,根本没有解决锁顺序的问题,只要系统负载高、线程调度出现波动就会再次复现,完全不能用于生产环境。

生产级稳定解决方案

方案1:启动阶段单线程预加载所有JDBC驱动(推荐,改动最小兼容性最好)

在启动任何业务线程之前,用主线程串行加载所有业务需要用到的JDBC驱动,从根源上避免多线程并发触发类加载和DriverManager初始化的锁竞争:

public class ch {
    public static void main(String[] args) throws Exception {
        // 主线程提前串行加载所有JDBC驱动,此时无多线程竞争,不会触发死锁
        Class.forName("ru.yandex.clickhouse.ClickHouseDriver");
        Class.forName("org.sqlite.JDBC");
        
        // 所有驱动加载完成后,再启动业务线程、初始化业务组件
        new OTTLCleaner().start();
        new CHThr().start();
        Spark.getInstance();
    }
}

这个方案的优势:

  • 改动量极小,只需要在程序入口加几行预加载代码
  • 后续新增其他数据库驱动时,只需要在预加载列表追加一行对应Class.forName代码即可
  • 没有任何时序依赖,不管线程怎么调度都不会触发类加载阶段的锁竞争,稳定性有完全保障

方案2:升级JDBC版本,移除手动加载驱动的代码

JDBC 4.0及以上版本已经支持SPI自动发现驱动,只要驱动jar包在classpath下,DriverManager初始化时会自动完成所有驱动的加载,不需要在业务代码里手动写Class.forName加载驱动。你可以直接删掉代码里所有手动加载驱动的逻辑,让驱动加载完全在DriverManager单线程的静态初始化阶段完成,自然不会出现并发锁问题。
注意这个方案需要确认你使用的ClickHouse、SQLite驱动版本支持JDBC4.0规范,2015年之后发布的主流JDBC驱动基本都满足这个要求。

额外优化点

你在DBHelper.SqliteConn()上加的static synchronized修饰完全没有必要,JDBC连接本身是线程隔离的,这个同步块只会不必要地降低并发性能,对避免死锁没有任何作用,可以直接移除。


内容的提问来源于stack exchange,提问作者Shashank Gb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 03:24:32