歡迎光臨
每天分享高質量文章

Java併發:隱藏的執行緒死鎖

(點選上方公眾號,可快速關註)


來源:ImportNew – 人曉

許多程式員都熟悉Java執行緒死鎖的概念。死鎖就是兩個執行緒一直相互等待。這種情況通常是由同步或者鎖的訪問(讀或寫)不當造成的。

Found one Java-level deadlock:

=============================

“pool-1-thread-2”:

  waiting to lock monitor 0x0237ada4 (object 0x272200e8, a java.lang.Object),

  which is held by “pool-1-thread-1”

“pool-1-thread-1”:

  waiting to lock monitor 0x0237aa64 (object 0x272200f0, a java.lang.Object),

  which is held by “pool-1-thread-2”

好訊息是最新的JVM通常會幫你檢測到這種死鎖現象,但它真的做到了嗎?最近一個執行緒死鎖問題影響了Oracle Service Bus的生產環境,這一訊息使得我們不得不重新審視這一經典問題,並找出“隱藏”死鎖存在的情況。本文將透過一個簡單的Java程式向大家講解一種非常特殊的鎖順序死鎖問題,這種死鎖在最新的JVM 1.7中並沒有被檢測到。文章末尾的影片講解了這段Java示例程式碼以及問題的解決方法。

http://javaeesupportpatterns.blogspot.com/2012/07/oracle-service-bus-stuck-thread-case.html

犯罪現場

通常,我習慣將出現嚴重Java併發問題的情況稱之為犯罪現場,在這裡你扮演一個偵查員的角色來解決問題。在這篇文章中,犯罪行為來源於客戶端IT環境執行中斷。你需要完成如下工作:

  • 收集證據、線索和事實(執行緒轉儲,日誌,業務影響,負載資訊…)

  • 審問目擊證人、諮詢相關領域專家(支撐團隊,交付團隊,供應商,客戶…)

接下來的調查工作為:分析收集到的資訊,並根據收集的證據建立一個或多個“嫌疑犯”名單。最終,將名單縮小到主要嫌犯或者說引發問題的根源者上。顯然,“凡不能被證明有罪者均無罪”的條例在這裡並不適用,這裡用到的規則恰恰相反。缺少證據會妨礙你找到問題的根源。下一步你將會看到JVM對死鎖檢測的缺乏並不能說明你無法解決這一問題。

嫌疑犯

在解決該問題的過程中,“嫌疑犯”被定義為具有以下執行樣式的應用程式或中介軟體程式碼:

  • 在ReentrantLock寫鎖使用之後使用普通鎖(執行執行緒#1)

  • 在使用普通鎖之後使用ReentrantLock 讀鎖(執行執行緒#2)

  • 當前的程式由兩個Java執行緒併發執行,但執行順序與正常順序相反

上面的鎖排序死鎖標準可以用下圖表示:


現在我們透過Java實體程式說明這一問題,同時檢視JVM執行緒轉儲輸出。

Java實體程式

上面的死鎖問題第一次是在Oracle OSB問題事例中發現的。之後,我們透過實體程式重建了該死鎖。你可以從這裡下載程式的原始碼。該程式只是簡單的建立了兩個執行緒,每個執行緒有不同的執行路徑,並且以不同的順序嘗試獲取共享物件的鎖。我們還建立了一個死鎖執行緒用來監控和記錄。現在,下麵的java類中實現了兩個不同的執行路徑。

https://docs.google.com/file/d/0B6UjfNcYT7yGbmllUEVEM2dFWTQ/edit

package org.ph.javaee.training8;

 

import java.util.concurrent.locks.ReentrantReadWriteLock;

 

/**

 * A simple thread task representation

 * @author Pierre-Hugues Charbonneau

 *

 */

public class Task {

 

       // Object used for FLAT lock

       private final Object sharedObject = new Object();

       // ReentrantReadWriteLock used for WRITE & READ locks

       private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

 

       /**

        *  Execution pattern #1

        */

       public void executeTask1() {

 

             // 1. Attempt to acquire a ReentrantReadWriteLock READ lock

             lock.readLock().lock();

 

             // Wait 2 seconds to simulate some work…

             try { Thread.sleep(2000);}catch (Throwable any) {}

 

             try {              

                    // 2. Attempt to acquire a Flat lock…

                    synchronized (sharedObject) {}

             }

             // Remove the READ lock

             finally {

                    lock.readLock().unlock();

             }           

 

             System.out.println(“executeTask1() :: Work Done!”);

       }

 

       /**

        *  Execution pattern #2

        */

       public void executeTask2() {

 

             // 1. Attempt to acquire a Flat lock

             synchronized (sharedObject) {                 

 

                    // Wait 2 seconds to simulate some work…

                    try { Thread.sleep(2000);}catch (Throwable any) {}

 

                    // 2. Attempt to acquire a WRITE lock                   

                    lock.writeLock().lock();

 

                    try {

                           // Do nothing

                    }

 

                    // Remove the WRITE lock

                    finally {

                           lock.writeLock().unlock();

                    }

             }

 

             System.out.println(“executeTask2() :: Work Done!”);

       }

 

       public ReentrantReadWriteLock getReentrantReadWriteLock() {

             return lock;

       }

}

一旦程式引起執行緒死鎖,JVM虛擬機器就會產生如下的執行緒轉儲輸出。

死鎖根源:ReetrantLock 讀鎖行為

我們發現在這一問題上主要和ReetrantLock讀鎖的使用有關。讀鎖通常不會被設計成具有所有權的概念(詳細資訊)。由於執行緒沒有記錄讀鎖,造成了HotSpot JVM死鎖檢測器的邏輯無法檢測到涉及讀鎖的死鎖。自發現該問題以後,JVM做了一些改進,但是我們發現JVM仍然不能檢測到這種特殊場景下的死鎖。現在,如果我們把程式中讀鎖替換成寫鎖,JVM就會檢測到這種死鎖問題,這是為什麼呢?

Found one Java-level deadlock:

=============================

“pool-1-thread-2”:

  waiting for ownable synchronizer 0x272239c0, (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync),

  which is held by “pool-1-thread-1”

“pool-1-thread-1”:

  waiting to lock monitor 0x025cad3c (object 0x272236d0, a java.lang.Object),

  which is held by “pool-1-thread-2”

 

Java stack information for the threads listed above:

===================================================

“pool-1-thread-2”:

       at sun.misc.Unsafe.park(Native Method)

       – parking to wait for  <0x272239c0> (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync)

       at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)

       at java.util.concurrent.locks.AbstractQueuedSynchronizer.

parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)

       at java.util.concurrent.locks.AbstractQueuedSynchronizer.

acquireQueued(AbstractQueuedSynchronizer.java:867)

       at java.util.concurrent.locks.AbstractQueuedSynchronizer.

acquire(AbstractQueuedSynchronizer.java:1197)

       at java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock.lock(ReentrantReadWriteLock.java:945)

       at org.ph.javaee.training8.Task.executeTask2(Task.java:54)

       – locked <0x272236d0> (a java.lang.Object)

       at org.ph.javaee.training8.WorkerThread2.run(WorkerThread2.java:29)

       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)

       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)

       at java.lang.Thread.run(Thread.java:722)

“pool-1-thread-1”:

       at org.ph.javaee.training8.Task.executeTask1(Task.java:31)

       – waiting to lock <0x272236d0> (a java.lang.Object)

       at org.ph.javaee.training8.WorkerThread1.run(WorkerThread1.java:29)

       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)

       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)

       at java.lang.Thread.run(Thread.java:722)

這是因為寫鎖能被JVM跟蹤,這點和普通鎖相似。這就意味著JVM死鎖檢測器能夠檢測如下情況的死鎖:

  • 物件監視器上涉及到普通鎖的死鎖

  • 和寫鎖相關的涉及到鎖定的可同步的死鎖

由於執行緒缺少對讀鎖的跟蹤造成這種場景下JVM無法檢測到死鎖,這樣增加瞭解決死鎖問題的難度。我推薦你讀一下Doug Lea關於這個問題的評論。由於一些潛在的死鎖會被忽略,在2005年人們再次提出是否有可能增加執行緒對讀鎖的跟蹤。如果你遇到了涉及讀鎖的隱藏死鎖,試試下麵的建議:

  • 仔細分析執行緒呼叫的跟蹤堆疊,它可以揭示一些程式碼可能獲取讀鎖同時防止其他執行緒獲取寫鎖

  • 如果你是程式碼的擁有者,呼叫lock.getReadLockCount的方法跟蹤讀鎖的計數

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6207928

非常期待你的反饋,尤其是那些遇到過讀鎖造成死鎖的開發者。最後,看看下麵的影片,我們透過執行和監控我們的實體程式說明瞭本文討論的問題。

觀看影片請自備扶梯:Java concurrency: the hidden thread deadlocks

https://www.youtube.com/watch?v=wdS1azOEIgc

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂