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
相关产品推荐
相关产品推荐

