羣裏有個大神(你假笨)再講解工做中碰到的一個死鎖問題.html
這個是大神後來總結的文章:http://lovestblog.cn/blog/2014/07/08/jdk-sql-deadlock/ java
狀況是這樣的:mysql
項目碰到多線程初始化JDBC驅動時,產生死鎖,以下實例所示: (個人環境: JDK1.7.0_45, msql_jdbc:mysql-connector-java-5.1.29)sql
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代碼塊上,oracle
下面是發生死鎖時卡死的大概代碼位置ide
線程A:靜態代碼塊主動註冊驅動spa
線程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