问题描述

当我们一个系统既需要mysql驱动,也需要oracle驱动的时候,在并发加载初始化这些驱动类的过程中产生死锁的可能性非常大,下面是一个模拟的例子,对于Thread2的实现其实是jdk里java.sql.DriverService的逻辑,也是我们第一次调用java.sql.DriverManager.registerDriver注册一个驱动实例要走的逻辑(jdk1.6下),不过这篇文章是使用我们生产环境的一个系统的线程dump和内存dump为基础进行分析展开的。

如果以上代码运行过程中发现有线程一直卡死在Class.forName的调用里,那么说明问题已经重现了。

先上两张图

内存态线程堆栈

线程堆栈

存疑点

仔细看看上面的线程dump分析和内存dump分析里的线程分析模块,您可能会有如下两个疑惑:

  • 【为什么线程[Thread-0]一直卡在Class.forName的位置】:这有点出乎意料,做一个类加载要么找不到抛出ClassNotFoundException,要么找到直接返回,为什么会一直卡在这个位置呢?

  • 【明明[Thread-0]注册的是mysql驱动为什么会去加载Odbc的驱动类】:通过[Thread-0]在栈上看倒数第二帧展开看到传入Class.forName的参数是com.mysql.jdbc.Driver,然后展开栈上顺序第二帧,看到传入的参数是sun.jdbc.odbc.JdbcOdbcDriver,这意味着在对mysql驱动类做加载初始化的过程中又触发了JdbcOdbc驱动类的加载

疑惑点解释

疑惑二:

第一个疑惑我们先留着,先解释下第二个疑惑,大家可以对照堆栈通过反编译rt.jar还有ojdbc6-11.2.0.3.0.jar看具体的代码

驱动类加载过程简要介绍:

当要注册某个sql驱动的时候是通过调用java.sql.DriverManager.registerDriver来实现的(注意这个方法加了synchronized关键字,后面解释第一个疑惑的时候是关键),而这个方法在第一次执行过程中,会在当前线程classloader的classpath下寻找所有/META-INF/services/java.sql.Driver文件,这个文件在mysql和oracle驱动jar里都有,里面写的是对应的驱动实现类名,这种机制是jdk提供的spi实现,找到这些文件之后,依次使用Class.forName(driverClassName, true, this.loader)来对这些驱动类进行加载,其中第二个参数是true,意味着不仅仅做一次loadClass的动作,还会初始化该类,即调用包含静态块的< clinit >方法,执行完之后才会返回,这样就解释了第二个疑惑,在mysql驱动注册过程中还会对odbc驱动类进行加载并初始化

感想:
其实我觉得这种设计有点傻,为什么要干和自己不相关的事情呢,画蛇添足的设计,首先类初始化的开销是否放到一起做并没有多大区别,其次正由于这种设计导致了今天这个死锁的发生

疑惑一:

现在来说第一个疑惑,为什么会一直卡在Class.forName呢,到底卡在哪里,于是再通过jstack -m 命令将jvm里的堆栈也打印出来,如下所示

我们看到其实正在做类的初始化动作,并且线程正在调用ObjectSynchronizer::waitUninterruptibly一直没返回,在看这方法的调用者instanceKlass1::initialize_impl,我们找到源码位置如下:

类的初始化过程:

当某个线程获得机会对某个类进行初始化的时候(请看上面的Step 6),会设置这个类的init_state属性为being_initialized(如果初始化好了会设置为fully_initialized,异常的话会设置为initialization_error),还会设置init_thread属性为当前线程,在这个设置过程中是有针对这个类提供了一把互斥锁的,因此当有别的线程进来的时候会被拦截在外面,如果设置完了,这把互斥锁也释放了,但是因为这个类的状态被设置了,因此并发问题也得到了解决,当另外一个线程也尝试初始化这个类的时候会判断这个类的状态是不是being_initialized,并且其init_thread不是当前线程,那么就会一直卡在那里,也就是此次线程dump的线程所处的状态,正在初始化类的线程会调用< clinit >方法,如果正常结束了,那么就设置其状态为fully_initialized,并且通知之前卡在那里等待初始化完成的线程,然他们继续往下走(下一个动作就是再判断下状态,发现完成了就直接return了)

猜想:

在了解了上面的过程之后,于是我们猜测两种可能

  • 第一,这个类的状态还是being_intialized,还在while循环里没有跳出来

  • 第二,事件通知机制出现了问题,也就是pthread_cond_wait和pthread_cond_signal之间的通信过程出现了问题。

不过第二种可能性非常小,比较linux久经考验了,那接下来我们验证其实是第一个猜想

验证:

我们通过GDB attach的方式连到了问题机器上(好在机器没有挂),首先我们要找到具体的问题线程,我们通过上面的jstack -m命令看到了线程ID是5738,然后通过info threads找到对应的线程,并得到它的序号14

然后通过thread 14切换到对应的线程,并通过bt看到了如下的堆栈,正如我们想象的那样,正在做类的初始化,一直卡在那里

我们通过f 6选择第7帧,在通过disassemble反汇编该帧,也就是对instanceKlass::initialize_impl ()这个方法反汇编

从上面的注释我们其实得出了,我们要看当前类的初始化状态,那就是看eax寄存器偏移0xe0的位置的值,而eax其实就是ebp寄存器偏移0xfffffff4位置的值,于是我们通过如下地址内存查到得到是4

而4其实代表的就是being_initialized这个状态,代码如下

从这于是我们验证了第一个猜想,其实是状态一直没有变更,因此一直卡在那里,为了更进一步确认这个问题,要是我们能找到该类的init_thread线程id就更清楚了,拿到这个ID我们就能看到这个线程栈,就知道它在干什么了,但是很遗憾,这个很难获取到,至少我一直没有找到办法,因为线程ID在线程对象里一直没有存,都是调用的os函数来获取的,得换个思路。

突然发现instanceKlass.hpp代码中得知两个属性原来是相邻的(init_state和init_thread),于是断定下一个地址的值就代表是这个线程对象了,但是其属性何其多,找到想要的太不易了,最主要的是还担心自己看的代码和服务器上的jvm代码不一致,这样更蛋疼了,于是继续查看Thread.hpp中的JavaThread类,找到个关键字0xDEAD-2=0xDEAB,这个有可能是volatile TerminatedTypes _terminated属性的值,于是把线程对象打印出来,果然查到了关键字0xDEAB

因此顺着这个属性继续往上找,找到了_thread_state表示线程状态的值(向上偏移三个字),0x0000000a,即10,然后查看代码知道原来线程是出于block状态

JavaThreadState

这样一来查看下线程dump,发现Thread-1正好处于BLOCKED状态,也就是说Thread-1就是那个正在对mysql驱动类做初始化的线程,这说明Thread-0和Thread-1成功互锁了

于是我们展开Thread-1,看到- waiting to lock <0x71ae2ec0> (a java.lang.Class for java.sql.DriverManager),该线程正在等待java.sql.DriverManager类型锁,而blocked在那里,而这个类型锁是被Thread-0线程持有的,从Thread-1这个线程堆栈来看它其实也是在做Class.forName动作,并且通过Thread-1,展开第四帧我们可以看到其正在对加载sun.jdbc.odbc.JdbcOdbcDriver

问题现场遐想:

于是我们大胆设想一个场景,Thread-1先获取到初始化sun.jdbc.odbc.JdbcOdbcDriver的机会,然后在执行sun.jdbc.odbc.JdbcOdbcDriver这个类的静态块的时候调用DriverManager.registerDriver(new Driver());,而该方法之前已经提到了是会加同步锁的,再想象一下,在这个这个静态块之前,并且设置了sun.jdbc.odbc.JdbcOdbcDriver类的初始化状态为being_initialized之后,Thread-0这个线程执行到了卡在的那个位置,并且我们从其堆栈可以看出它已经持有了java.sql.DriverManager这个类型的锁,因此这两个线程陷入了互锁状态

解决方案

解决方案目前想到的是将驱动类的加载过程变成单线程加载,不存在并发情况就没问题了

推荐阅读:

如此火爆的ZooKeeper,到底如何选主?

服务刚启动就 Old GC,要闹哪样?

JDK的sql设计不合理导致的驱动类初始化死锁问题的更多相关文章

  1. “秒杀”问题的数据库和SQL设计【转载】

    “秒杀”问题的数据库和SQL设计 APRIL 21ST, 2015 问题的来源 完全不考虑一致性的方案 表结构 方案 存在的问题 保证单用户不会重复购买 解决超卖问题 方案 优化 提高性能了 鱼与熊掌 ...

  2. SQL Server 2008过期导致MSSQLSERVER服务无法启动现象

    SQL Server 2008过期导致MSSQLSERVER服务无法启动现象:安装的是SQL Server 2008评估版,180天的试用期后,MSSQLSERVER服务就无法启动,手动启动就报告17 ...

  3. rac数据库默认sql tuning advisor,导致大量library cache lock

    rac数据库默认sql tuning advisor,导致大量library cache lock 问题现象:客户反映周六周日固定十点钟,一个程序会特别慢(大概10分钟),平时1到2秒.查看当时的日志 ...

  4. 设计、定义并实现Complex类

    设计.定义并实现Complex类 #include <iostream> #include <cmath> using namespace std; class MyCompl ...

  5. srping mvc 集成CXF 导致类初始化两遍

    cxf依赖于spring的ContextLoaderListener,而spring mvc 则依赖于DispatcherServlet. 初始化DispatcherServlet的时候会依赖初始化一 ...

  6. 设计一个 Java 程序,自定义异常类,从命令行(键盘)输入一个字符串,如果该字符串值为“XYZ”。。。

    设计一个 Java 程序,自定义异常类,从命令行(键盘)输入一个字符串,如果该字符串值为“XYZ”,则抛出一个异常信息“This is a XYZ”,如果从命令行输入 ABC,则没有抛出异常.(只有 ...

  7. PHP 进阶篇:面向对象的设计原则,自动加载类,类型提示,traits,命名空间,spl的使用,反射的使用,php常用设计模式 (麦子学员 第三阶段)

    以下是进阶篇的内容:面向对象的设计原则,自动加载类,类型提示,traits,命名空间,spl的使用,反射的使用,php常用设计模式 ================================== ...

  8. 在sql设计中没法修改表结构

    在做练习的时候经常表没设计好,后来有要去数据库修改表结构但是没词用界面修改的时候都会提示要保存 转自http://www.57xue.com/ItemView/Sql/2016061600160.ht ...

  9. PlSqlDev中执行INSERT SQL语句包含&符号导致数据异常

    在PLSQL Developer中执行Insert语句时提示如下信息: 当时未注意,直接点击OK按钮继续. 导入数据后查看发现部分数据中的参数丢失了一部分,呈以下规律: . 而正常应为: . 经询问大 ...

随机推荐

  1. OpenCV使用:加载图片时报错 0x00007FFC1084A839 处(位于 test1.exe 中)有未经处理的异常: Microsoft C++ 异常: cv::Exception,位于内存位置 0x00000026ABAFF1A8 处。

    加载图片代码为: #include<iostream> #include <opencv2/core/core.hpp> #include <opencv2/highgu ...

  2. C++走向远洋——58(项目二3、动物这样叫、改进版)

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  3. openpyxl(python操作Excel)

    一.安装 >>> pip install openpyxl import openpyxl 二.常用操作 1.创建与保存一个工作簿 wb = openpyxl.Workbook() ...

  4. py函数式编程

    函数式编程把计算视为函数而非指令,纯函数式编程不需要变量,没有副作用,测试简单,python支持的函数式编程不是纯函数式编程,允许有变量存在,支持高阶函数,支持闭包,有限度的支持匿名函数 变量可以指向 ...

  5. Java 集合、数组排序

    在平时开发的过程中,经常会遇到需要对数组.集合中元素按规则进行排序,本文记录在开发过程中可能遇到的情况以及相关的完整代码示例. 知识点 Comparable<T>接口 实现了该接口的对象, ...

  6. Echarts轻松入门,内附踩坑秘籍

    首先介绍一下我们的主角ECharts ECharts,一个纯 Javascript 的图表库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Fir ...

  7. JVM03——四种垃圾回收算法,你都了解了哪几种

    在之前的文章中,已经为各位带来了JVM的内存结构与堆内存的相关介绍,今天将为为各位详解JVM垃圾回收与算法.关注我的公众号「Java面典」了解更多 Java 相关知识点. 如何确定垃圾 想要回收垃圾, ...

  8. go学习第五天、运算符

    算术运算符 下表列出了所有Go语言的算术运算符.假定 A 值为 10,B 值为 20 运算符 描述 实例 + 相加 A + B 输出结果 30 - 相减 A - B 输出结果 -10 * 相乘 A * ...

  9. 微博立场检测 60分Baseline

    AI研习社最近举办了一个比赛--微博立场检测,实际上就是一个NLP文本分类的比赛 Baseline-FastText 我的Baseline方法用的是pkuseg分词+FastText,最好成绩是60, ...

  10. windows git pull或者push代码时弹出安全框解决办法

    目录 1.打开控制面板->凭据管理器>删除已有的git凭证 2.新增一个凭证 @ 如果报如下错误: 1.打开控制面板->凭据管理器>删除已有的git凭证 2.新增一个凭证 网络 ...