JDK的sql設計不合理致使的驅動類初始化死鎖問題

問題描述

當咱們一個系統既須要mysql驅動,也須要oracle驅動的時候,在併發加載初始化這些驅動類的過程當中產生死鎖的可能性很是大,下面是一個模擬的例子,對於Thread2的實現實際上是jdk裏java.sql.DriverService的邏輯,也是咱們第一次調用java.sql.DriverManager.registerDriver註冊一個驅動實例要走的邏輯(jdk1.6下),不過這篇文章是使用咱們生產環境的一個系統的線程dump和內存dump爲基礎進行分析展開的。java

image.png

若是以上代碼運行過程當中發現有線程一直卡死在Class.forName的調用裏,那麼說明問題已經重現了。mysql

先上兩張圖linux

內存態線程堆棧sql

image.png

線程堆棧sass

image.png

存疑點

仔細看看上面的線程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驅動類的加載oracle

疑惑點解釋

疑惑二:

第一個疑惑咱們先留着,先解釋下第二個疑惑,你們能夠對照堆棧經過反編譯rt.jar還有ojdbc6-11.2.0.3.0.jar看具體的代碼jvm

驅動類加載過程簡要介紹:函數

當要註冊某個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裏的堆棧也打印出來,以下所示

image.png

咱們看到其實正在作類的初始化動做,而且線程正在調用ObjectSynchronizer::waitUninterruptibly一直沒返回,在看這方法的調用者instanceKlass1::initialize_impl,咱們找到源碼位置以下:

image.png

類的初始化過程:

當某個線程得到機會對某個類進行初始化的時候(請看上面的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

image.png

而後經過thread 14切換到對應的線程,並經過bt看到了以下的堆棧,正如咱們想象的那樣,正在作類的初始化,一直卡在那裏

image.png

咱們經過f 6選擇第7幀,在經過disassemble反彙編該幀,也就是對instanceKlass::initialize_impl ()這個方法反彙編

image.png

從上面的註釋咱們其實得出了,咱們要看當前類的初始化狀態,那就是看eax寄存器偏移0xe0的位置的值,而eax其實就是ebp寄存器偏移0xfffffff4位置的值,因而咱們經過以下地址內存查到獲得是4

image.png

而4其實表明的就是being_initialized這個狀態,代碼以下

image.png

從這因而咱們驗證了第一個猜測,實際上是狀態一直沒有變動,所以一直卡在那裏,爲了更進一步確認這個問題,要是咱們能找到該類的init_thread線程id就更清楚了,拿到這個ID咱們就能看到這個線程棧,就知道它在幹什麼了,可是很遺憾,這個很難獲取到,至少我一直沒有找到辦法,由於線程ID在線程對象裏一直沒有存,都是調用的os函數來獲取的,得換個思路。

忽然發現instanceKlass.hpp代碼中得知兩個屬性原來是相鄰的(init_state和init_thread),因而判定下一個地址的值就表明是這個線程對象了,可是其屬性何其多,找到想要的太不易了,最主要的是還擔憂本身看的代碼和服務器上的jvm代碼不一致,這樣更蛋疼了,因而繼續查看Thread.hpp中的JavaThread類,找到個關鍵字0xDEAD-2=0xDEAB,這個有多是volatile TerminatedTypes _terminated屬性的值,因而把線程對象打印出來,果真查到了關鍵字0xDEAB

image.png

所以順着這個屬性繼續往上找,找到了_thread_state表示線程狀態的值(向上偏移三個字),0x0000000a,即10,而後查看代碼知道原來線程是出於block狀態

image.png

JavaThreadState

image.png

這樣一來查看下線程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,要鬧哪樣?

相關文章
相關標籤/搜索