一個JDBC驅動注冊死鎖問題總結
群里有個大神(你假笨)再講解工作中碰到的一個死鎖問題.
這個是大神后來總結的文章: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截圖
線程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.
主要過程就是上面的(遞歸情況自己看去.....................)
好, 現在我們來對著上面的死鎖例子來分析.
假設這樣一個場景(以下步驟按時間順序):
線程A: 調用Class.forName方法,第二個參數為true,表示如果類沒有初始化則初始化,
線程A: com.mysql.jdbc.Driver準備初始化, 獲取鎖(LDriver), 當剛剛獲取到鎖, (也就是剛剛進入到static代碼塊中,還沒執行任何操作.)
線程B: 開始執行,因為調用了.java.sql.DriverManager.getLoginTimeout()這個方法,然后得保證先加載DriverManager類,
線程B: 加載DriverManager.class , 獲取鎖(LDriverManager), 執行static代碼塊, 然后找到所有的jdbc驅動實現class(其中包含com.mysql.jdbc.Driver)
通過調用驅動實現class.newInstance()方法來判斷是不是實現了java.sql.Driver接口, (即:com.mysql.jdbc.Driver.newInstance())
線程B: 因為調用了com.mysql.jdbc.Driver.newInstance(), 所以他要保證com.mysql.jdbc.Driver已經被初始化, 所以他去申請獲取鎖(LDriver),
但是這個時候鎖(LDriver)被線程A 鎖占用著,所以阻塞.
線程A: 繼續執行,然后準備執行java.sql.DriverManager.registerDriver(new Driver()); 所以要保證DriverManager已經被初始化, 于是申請鎖(LDriverManager)
但是這個時候鎖(LDriverManager)被線程B占用著,所以阻塞.
于是.......................................................................................................死鎖了......
所以還是 避免手動的調用Class.forName()加載驅動,特別是多線程情況.
問題并不難,難的是碰到問題后去查找.
碰到這個問題,我想到的就是通過堆棧去找代碼,然后對照源碼一步步的分析猜測各種情況,
但是這樣不能百分百的找出問題, 而且對個人的技術也有一定的要求.
但是大神就是大神,通過分析內存,查看虛擬機源碼,反匯編,等等操作,雖然過程繁瑣了點,但是無敵啊~~~~膜拜ing
來自:http://my.oschina.net/haogrgr/blog/289120