关于J2EE中死锁问题的研究(2)
生活随笔
收集整理的這篇文章主要介紹了
关于J2EE中死锁问题的研究(2)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
跨資源死鎖情形之1:客戶端的增加導致資源池耗盡
我們要介紹的第一種死鎖情形是單純由于負載而造成的,即資源池太小,而每個線程需要的資源超過了池中的可用資源。例如,考慮一個使用數據庫連接的EJB調用,執行一個嵌套的EJB調用(使用同一連接池中不同的數據庫連接)。例如,如果該嵌套的EJB調用聲明為RequiresNew,就會出現死鎖情形。
在正常負載或者有足夠大小的連接池的情況下,EJB調用將從池中獲取一個數據庫連接,然后調用嵌套的EJB。嵌套的EJB調用將從池中獲取另一個數據庫連接,提交內部事務,然后向池返回連接。外部EJB調用將提交自己的事務,并向池返回其連接。
但是,假設連接池最多有10個連接,同時有10個對外部EJB的并發調用。這些線程中每一個都需要一個數據庫連接用來清空池。現在,每個線程都執行嵌套的EJB調用(需要獲取第二個數據庫連接)。則所有線程都不能繼續,但又都不放棄自己的第一個數據庫連接。這樣,10個線程都將被死鎖。
如果研究此類死鎖情形,會發現線程轉儲中有大量等待獲取資源的線程,以及同等數量的空閑且未阻塞的活動數據庫連接。當應用程序死鎖時,如果可以在運行時檢測連接池,應該能確認連接池實際上已空。
修復此類死鎖的方法包括:增加連接池的大小或者重構代碼,以便單個線程不需要同時使用很多數據庫連接。如果單線程需要的最大數據庫連接數為M,且可能的最大并發調用數為N,則要避免此問題,在池中所需的最小連接數為(N*(M01))+1。或者可以設置內部EJB調用以使用不同的連接池,即使外部調用的連接池為空,內部調用也能使用自己的連接池繼續。
跨資源死鎖情形之2:單線程、多沖突數據庫連接
對同一線程執行嵌套的EJB調用時還會出現第二種跨資源死鎖情形,此情形即使在非高負載系統中通常也會發生。同上面的示例一樣,兩個EJB調用使用不同的連接來連接到同一個數據庫。因為只有嵌套調用完成后調用方才能繼續,所以調用方的數據庫連接實際上被嵌套調用的數據庫連接阻塞了,雖然數據庫沒有注意到這種關系。如果第一個(外部)連接已獲取第二個(內部)連接所需要的數據庫鎖,則第二個連接將永久阻塞第一個連接,并等待第一個連接被提交或回滾,這就出現了死鎖情形。因為數據庫沒有注意到兩個連接之間的關系,所以數據庫不會將此情形檢測為死鎖。
作為一個具體的示例,考慮一個數據加載EJB調用。此EJB調用獲取一個大型對象,并在不同階段中將其保存在數據庫中。當它執行數據加載時,它會更新一個單獨的表,以記錄掛起數據加載操作的狀態。我們希望狀態更新立即可見,但不希望在未完成的狀態下看到加載的數據,所以要通過調用“RequiresNew” EJB來完成。總的來說,這種不完善的數據加載方法如清單1中的代碼所示。
清單1 public void bulkLoadData(DataBatch batch) { int batchId = batch.getId(); // Since this executeUpdate call doesn誸 happen in a separate // transaction, it wouldn't be visible anyway, but the effect is // far worse: a cross-resource deadlock. executeUpdate("update batch_status set status='Started' " + "where batch_id=" + batchId); validateData(batch); updateBatchStatus(batchId, "Validated"); // RequiresNew EJB call loadDataStage1(batch); updateBatchStatus(batchId, "Stage 1 complete"); // RequiresNew EJB call loadDataStage2(batch); updateBatchStatus(batchId, "Stage 2 complete"); // RequiresNew EJB call finalizeDataLoad(batch); updateBatchStatus(batchId, "Complete"); // RequiresNew EJB call } 在上面的示例中,使用updateBatchStatus方法執行“RequiresNew” EJB調用實際上可以更新batch_status數據庫表,即使沒有看到當前事務的效果,也能立即看到狀態的改變。對executeUpdate的調用不是EJB調用,所以它和bulkLoadData的其他部分在同一個事務中執行。
如上所述,即使不存在并發,此代碼也將導致死鎖。當bulkLoadData調用executeUpdate方法時,它更新現有的數據庫行,這涉及為該行獲取寫鎖。對updateBatchStatus的嵌套EJB調用將在單獨的數據庫連接上執行,并嘗試執行一個非常相似的查詢,但它將阻塞,因為不能獲取必需的寫鎖。從數據庫的角度來說,只要提交或回滾第一個連接的事務,第二個連接就可以繼續。但是,Java虛擬機不允許在完成所有對updateBatchStatus的調用前完成bulkLoadD調用,這樣就出現了死鎖情形。
該示例表明,一個更新會阻塞另一個更新,所以它會在任何數據庫中導致死鎖。如果初始更新查詢是一個簡單的選擇查詢,那么該示例僅在使用基于鎖的并發控制的數據庫上導致死鎖,在這種數據庫中,一個連接的讀鎖可以阻止另一個連接獲取寫鎖。不管在哪種情況下,此類死鎖即不依賴于同步,也不依賴于負載,而且線程轉儲將顯示一個等待數據庫響應的Java線程,但該線程與兩個有效的數據庫連接相關聯。在這些數據庫連接中,有一個將處于空閑狀態,但會阻塞其他連接。
此情形有多種具體的變種,可以涉及多個線程和兩個以上的數據庫連接。例如,外部EJB調用的數據庫連接可能已經獲取了數據庫鎖,該鎖阻塞了另一個無關數據庫連接的繼續,但這個無關數據庫連接已經獲取了阻塞嵌套EJB調用的數據庫操作的鎖。這個特例是依賴于同步的,并將顯示多個等待數據庫響應的Java線程。其中至少有一個Java線程將與兩個活動數據庫連接相關聯。
跨資源死鎖情形之3:Java虛擬機鎖與數據庫鎖相沖突
第三種死鎖情形發生在數據庫鎖與Java虛擬機鎖并存的時候。在這種情況下,一個線程占有一個數據庫鎖并嘗試獲取Java虛擬機鎖(嘗試進入同步的鎖)。同時,另一個線程占有Java虛擬機鎖并嘗試獲取數據庫鎖。再次地,數據庫發現一個連接阻塞了另一個連接,但由于無法阻止連接繼續,所以不會檢測到死鎖。Java虛擬機發現同步的鎖中有一個線程,并有另一個嘗試進入的線程,所以即使Java虛擬機能檢測到死鎖并對它們進行處理,它還是不會檢測到這種情況。
為了說明此種死鎖情形,我們以一個簡單的(不完善的)read-through cache為例。該cache是數據庫表中備份的HashMap。如果出現緩存命中,它就從HashMap返回一個值。但在緩存缺失的情況下,它將從數據庫讀取值,將其添加到HashMap,然后返回該值,如清單2所示。
清單 2 public class SimpleCache { private Map cache = new HashMap(); public synchronized Object get(String key) { if (cache.containsKey(key)) { return cache.get(key); } else { Object value = queryForValue(key); cache.put(key, value); return value; } } private Object queryForValue(String key) { return executeQuery("select value from cache_table " + "where key='" + key + "'"); } public synchronized void clearCache() { cache.clear(); } // other methods omitted for brevity }
這是一個簡單的遍歷cache。注意:get()方法是同步的,這是因為我們訪問了非線程安全容器,并要求containsKey/put組合在緩存缺失時是原子性的。
該cache相當簡單易懂:它約定,如果更改支持緩存的表中的數據,則應調用clearCache(),這樣緩存就可以避免處理陳舊的數據。產生的緩存缺失將相應地重新進入緩存。
我們現在來考慮可以更改此數據并清除緩存的代碼:
public void updateData(String key, String value) { executeUpdate("update cache_table set value='" + value + "' where key='" + key + "'"); SimpleCache.getInstance().clearCache(); } 上面的代碼在簡單的例子中能正常運行。但是,在使用基于鎖的并發控制的數據庫中,updateData中的查詢將阻止queryForValue中的選擇查詢的執行,因為update語句將獲取一個寫鎖,從而阻止選擇查詢獲取同一數據行上的讀鎖。如果同步沒有問題,一個線程可以嘗試讀取緩存中的給定值,并在另一個線程在數據庫中更新該值時得到緩存缺失。如果數據庫先執行update語句,它將阻塞select語句繼續執行。但是,執行select語句的線程來自同步的get方法,所以它獲取了SimpleCache上的鎖。要返回updateData中的線程,它必須調用clearCache(),但不能獲取鎖(clearCache()是同步的)。
當處理此情形的實例時,將有一個等待數據庫響應的Java線程和一個等待獲取Java虛擬機鎖的線程。每個線程將與一個數據庫連接相關聯,其中一個連接阻塞另一個連接。修復方法是占有Java虛擬機鎖時避免執行數據庫操作,可以重寫leCache的get()方法,如下所示:
?
public Object get(String key) { synchronized(this) { if (cache.containsKey(key)) { return cache.get(key); } } Object value = queryForValue(key); synchronized(this) { cache.put(key, value); } return value; }
既然現在我們知道了會發生此死鎖情況,就可以使用Thread.holdsLock()向queryForValue方法添加檢查以嘗試避免死鎖情況:
private Object queryForValue(String key) { assert(!Thread.holdsLock(this)); return executeQuery(...); } 上例中的Thread.holdsLock()很有用,但是只有在我們知道需要留心哪個鎖時它才會發揮作用。如果有一個類似的方法可以確定當前線程占有哪個Java虛擬機鎖,那么會很有用。任何執行任何種類的RPC調用、數據庫訪問等的代碼片段都可以拋出異常或記錄警告,指示在占有Java虛擬機鎖時執行這些操作會有危險。
注意:雖然我們修復了上例中的死鎖問題,但它仍有缺陷,因為在提交updateData的事務之前清空了緩存。如果在調用clearCache后、提交updateData事務前出現緩存缺失,則該緩存將加載舊數據,因為新數據尚未可見。這里的修復方法是僅在提交更改后清空緩存。注意,這只在MVCC數據庫中發生。在基于鎖的數據庫中,掛起的update將阻塞緩存的讀操作,所以在提交update的事務后緩存才能讀取正確值。
我們要介紹的第一種死鎖情形是單純由于負載而造成的,即資源池太小,而每個線程需要的資源超過了池中的可用資源。例如,考慮一個使用數據庫連接的EJB調用,執行一個嵌套的EJB調用(使用同一連接池中不同的數據庫連接)。例如,如果該嵌套的EJB調用聲明為RequiresNew,就會出現死鎖情形。
在正常負載或者有足夠大小的連接池的情況下,EJB調用將從池中獲取一個數據庫連接,然后調用嵌套的EJB。嵌套的EJB調用將從池中獲取另一個數據庫連接,提交內部事務,然后向池返回連接。外部EJB調用將提交自己的事務,并向池返回其連接。
但是,假設連接池最多有10個連接,同時有10個對外部EJB的并發調用。這些線程中每一個都需要一個數據庫連接用來清空池。現在,每個線程都執行嵌套的EJB調用(需要獲取第二個數據庫連接)。則所有線程都不能繼續,但又都不放棄自己的第一個數據庫連接。這樣,10個線程都將被死鎖。
如果研究此類死鎖情形,會發現線程轉儲中有大量等待獲取資源的線程,以及同等數量的空閑且未阻塞的活動數據庫連接。當應用程序死鎖時,如果可以在運行時檢測連接池,應該能確認連接池實際上已空。
修復此類死鎖的方法包括:增加連接池的大小或者重構代碼,以便單個線程不需要同時使用很多數據庫連接。如果單線程需要的最大數據庫連接數為M,且可能的最大并發調用數為N,則要避免此問題,在池中所需的最小連接數為(N*(M01))+1。或者可以設置內部EJB調用以使用不同的連接池,即使外部調用的連接池為空,內部調用也能使用自己的連接池繼續。
跨資源死鎖情形之2:單線程、多沖突數據庫連接
對同一線程執行嵌套的EJB調用時還會出現第二種跨資源死鎖情形,此情形即使在非高負載系統中通常也會發生。同上面的示例一樣,兩個EJB調用使用不同的連接來連接到同一個數據庫。因為只有嵌套調用完成后調用方才能繼續,所以調用方的數據庫連接實際上被嵌套調用的數據庫連接阻塞了,雖然數據庫沒有注意到這種關系。如果第一個(外部)連接已獲取第二個(內部)連接所需要的數據庫鎖,則第二個連接將永久阻塞第一個連接,并等待第一個連接被提交或回滾,這就出現了死鎖情形。因為數據庫沒有注意到兩個連接之間的關系,所以數據庫不會將此情形檢測為死鎖。
作為一個具體的示例,考慮一個數據加載EJB調用。此EJB調用獲取一個大型對象,并在不同階段中將其保存在數據庫中。當它執行數據加載時,它會更新一個單獨的表,以記錄掛起數據加載操作的狀態。我們希望狀態更新立即可見,但不希望在未完成的狀態下看到加載的數據,所以要通過調用“RequiresNew” EJB來完成。總的來說,這種不完善的數據加載方法如清單1中的代碼所示。
清單1 public void bulkLoadData(DataBatch batch) { int batchId = batch.getId(); // Since this executeUpdate call doesn誸 happen in a separate // transaction, it wouldn't be visible anyway, but the effect is // far worse: a cross-resource deadlock. executeUpdate("update batch_status set status='Started' " + "where batch_id=" + batchId); validateData(batch); updateBatchStatus(batchId, "Validated"); // RequiresNew EJB call loadDataStage1(batch); updateBatchStatus(batchId, "Stage 1 complete"); // RequiresNew EJB call loadDataStage2(batch); updateBatchStatus(batchId, "Stage 2 complete"); // RequiresNew EJB call finalizeDataLoad(batch); updateBatchStatus(batchId, "Complete"); // RequiresNew EJB call } 在上面的示例中,使用updateBatchStatus方法執行“RequiresNew” EJB調用實際上可以更新batch_status數據庫表,即使沒有看到當前事務的效果,也能立即看到狀態的改變。對executeUpdate的調用不是EJB調用,所以它和bulkLoadData的其他部分在同一個事務中執行。
如上所述,即使不存在并發,此代碼也將導致死鎖。當bulkLoadData調用executeUpdate方法時,它更新現有的數據庫行,這涉及為該行獲取寫鎖。對updateBatchStatus的嵌套EJB調用將在單獨的數據庫連接上執行,并嘗試執行一個非常相似的查詢,但它將阻塞,因為不能獲取必需的寫鎖。從數據庫的角度來說,只要提交或回滾第一個連接的事務,第二個連接就可以繼續。但是,Java虛擬機不允許在完成所有對updateBatchStatus的調用前完成bulkLoadD調用,這樣就出現了死鎖情形。
該示例表明,一個更新會阻塞另一個更新,所以它會在任何數據庫中導致死鎖。如果初始更新查詢是一個簡單的選擇查詢,那么該示例僅在使用基于鎖的并發控制的數據庫上導致死鎖,在這種數據庫中,一個連接的讀鎖可以阻止另一個連接獲取寫鎖。不管在哪種情況下,此類死鎖即不依賴于同步,也不依賴于負載,而且線程轉儲將顯示一個等待數據庫響應的Java線程,但該線程與兩個有效的數據庫連接相關聯。在這些數據庫連接中,有一個將處于空閑狀態,但會阻塞其他連接。
此情形有多種具體的變種,可以涉及多個線程和兩個以上的數據庫連接。例如,外部EJB調用的數據庫連接可能已經獲取了數據庫鎖,該鎖阻塞了另一個無關數據庫連接的繼續,但這個無關數據庫連接已經獲取了阻塞嵌套EJB調用的數據庫操作的鎖。這個特例是依賴于同步的,并將顯示多個等待數據庫響應的Java線程。其中至少有一個Java線程將與兩個活動數據庫連接相關聯。
跨資源死鎖情形之3:Java虛擬機鎖與數據庫鎖相沖突
第三種死鎖情形發生在數據庫鎖與Java虛擬機鎖并存的時候。在這種情況下,一個線程占有一個數據庫鎖并嘗試獲取Java虛擬機鎖(嘗試進入同步的鎖)。同時,另一個線程占有Java虛擬機鎖并嘗試獲取數據庫鎖。再次地,數據庫發現一個連接阻塞了另一個連接,但由于無法阻止連接繼續,所以不會檢測到死鎖。Java虛擬機發現同步的鎖中有一個線程,并有另一個嘗試進入的線程,所以即使Java虛擬機能檢測到死鎖并對它們進行處理,它還是不會檢測到這種情況。
為了說明此種死鎖情形,我們以一個簡單的(不完善的)read-through cache為例。該cache是數據庫表中備份的HashMap。如果出現緩存命中,它就從HashMap返回一個值。但在緩存缺失的情況下,它將從數據庫讀取值,將其添加到HashMap,然后返回該值,如清單2所示。
清單 2 public class SimpleCache { private Map cache = new HashMap(); public synchronized Object get(String key) { if (cache.containsKey(key)) { return cache.get(key); } else { Object value = queryForValue(key); cache.put(key, value); return value; } } private Object queryForValue(String key) { return executeQuery("select value from cache_table " + "where key='" + key + "'"); } public synchronized void clearCache() { cache.clear(); } // other methods omitted for brevity }
這是一個簡單的遍歷cache。注意:get()方法是同步的,這是因為我們訪問了非線程安全容器,并要求containsKey/put組合在緩存缺失時是原子性的。
該cache相當簡單易懂:它約定,如果更改支持緩存的表中的數據,則應調用clearCache(),這樣緩存就可以避免處理陳舊的數據。產生的緩存缺失將相應地重新進入緩存。
我們現在來考慮可以更改此數據并清除緩存的代碼:
public void updateData(String key, String value) { executeUpdate("update cache_table set value='" + value + "' where key='" + key + "'"); SimpleCache.getInstance().clearCache(); } 上面的代碼在簡單的例子中能正常運行。但是,在使用基于鎖的并發控制的數據庫中,updateData中的查詢將阻止queryForValue中的選擇查詢的執行,因為update語句將獲取一個寫鎖,從而阻止選擇查詢獲取同一數據行上的讀鎖。如果同步沒有問題,一個線程可以嘗試讀取緩存中的給定值,并在另一個線程在數據庫中更新該值時得到緩存缺失。如果數據庫先執行update語句,它將阻塞select語句繼續執行。但是,執行select語句的線程來自同步的get方法,所以它獲取了SimpleCache上的鎖。要返回updateData中的線程,它必須調用clearCache(),但不能獲取鎖(clearCache()是同步的)。
當處理此情形的實例時,將有一個等待數據庫響應的Java線程和一個等待獲取Java虛擬機鎖的線程。每個線程將與一個數據庫連接相關聯,其中一個連接阻塞另一個連接。修復方法是占有Java虛擬機鎖時避免執行數據庫操作,可以重寫leCache的get()方法,如下所示:
?
public Object get(String key) { synchronized(this) { if (cache.containsKey(key)) { return cache.get(key); } } Object value = queryForValue(key); synchronized(this) { cache.put(key, value); } return value; }
既然現在我們知道了會發生此死鎖情況,就可以使用Thread.holdsLock()向queryForValue方法添加檢查以嘗試避免死鎖情況:
private Object queryForValue(String key) { assert(!Thread.holdsLock(this)); return executeQuery(...); } 上例中的Thread.holdsLock()很有用,但是只有在我們知道需要留心哪個鎖時它才會發揮作用。如果有一個類似的方法可以確定當前線程占有哪個Java虛擬機鎖,那么會很有用。任何執行任何種類的RPC調用、數據庫訪問等的代碼片段都可以拋出異常或記錄警告,指示在占有Java虛擬機鎖時執行這些操作會有危險。
注意:雖然我們修復了上例中的死鎖問題,但它仍有缺陷,因為在提交updateData的事務之前清空了緩存。如果在調用clearCache后、提交updateData事務前出現緩存缺失,則該緩存將加載舊數據,因為新數據尚未可見。這里的修復方法是僅在提交更改后清空緩存。注意,這只在MVCC數據庫中發生。在基于鎖的數據庫中,掛起的update將阻塞緩存的讀操作,所以在提交update的事務后緩存才能讀取正確值。
轉載于:https://www.cnblogs.com/xiefang1980/archive/2008/04/28/1174136.html
總結
以上是生活随笔為你收集整理的关于J2EE中死锁问题的研究(2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PDF阅读器Foxit Reader 2
- 下一篇: 在php中使用mb_substr($ro