一个JDBC驱动注册死锁问题总结

原创
2014/07/09 20:42
阅读数 7.2K

群里有个大神(你假笨)再讲解工作中碰到的一个死锁问题.

这个是大神后来总结的文章:http://lovestblog.cn/blog/2014/07/08/jdk-sql-deadlock/  

情况是这样的:

项目碰到多线程初始化JDBC驱动时,产生死锁,如下实例所示:   (我的环境: JDK1.7.0_45,  msql_jdbc:mysql-connector-java-5.1.29)

public class Temp {
	public static void main(String[] args) throws Exception {
		Thread a = new Thread(new ThreadA());
		Thread b= new Thread(new ThreadB());
		a.start();
		b.start();
	}
}

class ThreadA implements Runnable{
	@Override
	public void run() {
		try {
			Class.forName("com.mysql.jdbc.Driver", true, Thread.currentThread().getContextClassLoader());
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

class ThreadB implements Runnable{
	@Override
	public void run() {
		java.sql.DriverManager.getLoginTimeout();//这个调用只是为了加载DriverManager类
	}
}

发现程序出现死锁,以下是jconsole截图

线程A,可以看到卡死在mysql Driver的static代码块上.


线程B,可以看到也是卡死在DriverManager的static代码块上,


下面是发生死锁时卡死的大概代码位置


线程A:静态代码块主动注册驱动


线程B:静态代码块,主动加载所有驱动.

说明:这个方法会扫描classpath: /META-INF/services/java.sql.Driver 文件,该文件存放的是Driver的具体实现,例如mysql JDBC jar中该文件内容为文本: com.mysql.jdbc.Driver

也就是它会找出classpath下所有的jdbc驱动实现类,然后他会调用(省略了很多代码)

Class<?> c = Class.forName(cn, false, loader);   //装载驱动实现类,但是不初始化
S p = service.cast(c.newInstance());             //判断驱动实现类是不是实现了java.sql.Driver接口,(serveic == Driver.class)



要说明死锁会出现的原因,我们得先来了解下类初始化的过程, 具体见oracle文档:http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.2

我大概说下吧:

初始化Java类和接口需要保证线程安全, 因为有可能同一时间有多个线程同时初始化某一类或接口,但是语言要求一个类对于一个classloader只能初始化一次.

那么虚拟机是如何保证的呢? 没错, 加锁(-.-!

虚拟机给一个已经被加载的class定义了四个状态:  1.没有初始化, 2.正在被初始化, 3.已经被初始化, 4.初始化报错(比如static代码块内代码抛异常了).

当一个类发现他要初始化一个class时, 比如C, 它会去申请一把锁 LC, 获取到LC之后(没获取就阻塞), 他就开始查看状态了,

    如果当前状态是1(没有初始化), 就将状态改成2(正在初始化), 然后释放锁,然后开始初始化.

    如果当前状态是2(正在初始化), 就释放锁,然后block,等待被唤醒(当有线程完成初始化后,会获取锁,将状态改成3(初始化完成),然后唤醒阻塞在这里的线程)

    如果当前状态是3(已被初始化), 就释放锁,然后.................................直接用啥~~~~差点没反应过来......

    如果当前状态是4(异常状态),    就释放锁,然后抛个NoClassDefFoundError.

主要过程就是上面的(递归情况自己看去.....................)



好, 现在我们来对着上面的死锁例子来分析.

假设这样一个场景(以下步骤按时间顺序):

  1. 线程A:  调用Class.forName方法,第二个参数为true,表示如果类没有初始化则初始化, 

  2. 线程A:  com.mysql.jdbc.Driver准备初始化, 获取锁(LDriver),  当刚刚获取到锁, (也就是刚刚进入到static代码块中,还没执行任何操作.)

  3. 线程B:  开始执行,因为调用了.java.sql.DriverManager.getLoginTimeout()这个方法,然后得保证先加载DriverManager类,

  4. 线程B:  加载DriverManager.class , 获取锁(LDriverManager), 执行static代码块, 然后找到所有的jdbc驱动实现class(其中包含com.mysql.jdbc.Driver)

               通过调用驱动实现class.newInstance()方法来判断是不是实现了java.sql.Driver接口, (即:com.mysql.jdbc.Driver.newInstance())

  5. 线程B:  因为调用了com.mysql.jdbc.Driver.newInstance(), 所以他要保证com.mysql.jdbc.Driver已经被初始化, 所以他去申请获取锁(LDriver),

               但是这个时候锁(LDriver)被线程A 锁占用着,所以阻塞.

  6. 线程A:  继续执行,然后准备执行java.sql.DriverManager.registerDriver(new Driver()); 所以要保证DriverManager已经被初始化, 于是申请锁(LDriverManager)

               但是这个时候锁(LDriverManager)被线程B占用着,所以阻塞.


于是.......................................................................................................死锁了......


所以还是 避免手动的调用Class.forName()加载驱动,特别是多线程情况.


问题并不难,难的是碰到问题后去查找.

碰到这个问题,我想到的就是通过堆栈去找代码,然后对照源码一步步的分析猜测各种情况,

但是这样不能百分百的找出问题, 而且对个人的技术也有一定的要求.

但是大神就是大神,通过分析内存,查看虚拟机源码,反汇编,等等操作,虽然过程繁琐了点,但是无敌啊~~~~膜拜ing


展开阅读全文
打赏
5
82 收藏
分享
加载中
德胜博主

引用来自“pugwoo”的评论

getLoginTimeout是做什么的呢?为什么这么用呢?
这个方法只是个简单的get方法,没啥用.我这里就是要掉一个DriverManager的方法,随便啥方法都可以,这方法比较简单,所以我就调用了这方法.
2014/07/15 11:53
回复
举报
getLoginTimeout是做什么的呢?为什么这么用呢?
2014/07/15 08:34
回复
举报
德胜博主

引用来自“享开发”的评论

这个问题的原因看起来挺简单的 当时他们排查了挺长时间的 排查的过程 值得所有javaer借鉴 学习。其实 有些坑 路过了才会懂 很小的一个细节 都会让你折腾很长一段时间
顺便打个小广告 支付宝杭州中间件团队 正在招兵买马 欢迎有兴趣的同学加入我们的团队 感兴趣的欢迎私信我 条件合适的可以走内部推荐哦
谢谢
2014/07/11 17:58
回复
举报
德胜博主

引用来自“jobet”的评论

不要在线程中去加载驱动。驱动的注册类在这一块有保护的,不允许多次加载。
对,DriverManager里面有map, 驱动注册的时候,他会addIfAbsent.
2014/07/11 17:57
回复
举报
德胜博主

引用来自“ylmotol7”的评论

难道c3p0有这个问题?!
不清楚
2014/07/11 17:55
回复
举报
德胜博主

引用来自“FunkyYang”的评论

jdbc4.0规范中可以不显示去加载驱动,是否可以避免这个问题呢
4.0就是通过我上面说的扫描classpath:/META-INF/services/java.sql.Driver文件来做到的,所以你不用在class.forName了,应该就没有这个问题了.
2014/07/11 17:55
回复
举报
德胜博主

引用来自“惉惉自喜”的评论

闭锁可解,这个没必要看vm源码吧,用visualVM等工具一眼就看出来了
你牛逼,哈哈,知道我是谁不
2014/07/11 17:51
回复
举报
闭锁可解,这个没必要看vm源码吧,用visualVM等工具一眼就看出来了
2014/07/11 16:50
回复
举报
当时查找了好长时间。
2014/07/11 15:59
回复
举报
晕,话说我遇到过这个问题,确实这样子的。记得我多线程加载MYSQL、MSSQL、ORACLE....驱动。就死锁了。
2014/07/11 15:58
回复
举报
更多评论
打赏
17 评论
82 收藏
5
分享
返回顶部
顶部