一個JDBC驅動注冊死鎖問題總結

jopen 10年前發布 | 23K 次閱讀 JDBC Java開發

群里有個大神(你假笨)再講解工作中碰到的一個死鎖問題.

這個是大神后來總結的文章: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類 } }</pre>

發現程序出現死鎖,以下是jconsole截圖

一個JDBC驅動注冊死鎖問題總結

線程A,可以看到卡死在mysql Driver的static代碼塊上.

 

一個JDBC驅動注冊死鎖問題總結

線程B,可以看到也是卡死在DriverManager的static代碼塊上,

 

下面是發生死鎖時卡死的大概代碼位置

 

線程A:靜態代碼塊主動注冊驅動

一個JDBC驅動注冊死鎖問題總結

 

線程B:靜態代碼塊,主動加載所有驅動.

一個JDBC驅動注冊死鎖問題總結

說明:這個方法會掃描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

來自:http://my.oschina.net/haogrgr/blog/289120 

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!