1. 程式人生 > >一個由阻塞佇列引發的類死鎖案例

一個由阻塞佇列引發的類死鎖案例

之所以說是類死鎖,因為發生的現象幾乎與死鎖相同,程式將一直阻塞下去,但是又沒有形成環路。

本次介紹案例中,是阻塞佇列引起的。
阻塞佇列有一個特點:
佇列滿時, 往佇列放入元素會被阻塞;
佇列空時, 從佇列取出元素會被阻塞。

假設有一個共享阻塞佇列,和一把鎖lock。 生產和消費執行緒。
我們分析一下下面的場景:
1 生產執行緒 持有lock ,開始向佇列push資料(此時未執行push);
2 消費執行緒從佇列pop一個數據,並請求lock ;
3 其他執行緒 直接向佇列 push資料,直至佇列滿;
4 生產執行緒現在進行push資料。

至此,由於佇列已滿 生產執行緒的push將被阻塞,持有的lock 將不會釋放,消費執行緒也因為無法得到lock,也會阻塞,整個程式將會一直等待下去。

是不是特別容易理解? 這種情況我們使用jstack 檢視執行緒堆疊並不會告訴你有死鎖發生(這不算是個死鎖)。
下面用程式碼來演示這一過程:

public class DeadLockTest {
    static Object lock = new Object();
    static ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(2);

    static class ConsumerThread implements Runnable {

        @Override
        public
void run() { String res = null; while(true) { try { res = queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock) { System.out.println("consumer data ---"
+ res); } } } } static class ProducerThread implements Runnable { @Override public void run() { try { while (true) { synchronized (lock) { TimeUnit.SECONDS.sleep(2); queue.put("flush event"); System.out.println("put to queue..."); } TimeUnit.SECONDS.sleep(3); } } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { Thread consumer = new Thread(new ConsumerThread()); Thread producer = new Thread(new ProducerThread()); queue.put("first event"); queue.put("second event"); producer.start(); TimeUnit.SECONDS.sleep(1); consumer.start(); queue.put("third event"); } }

我們執行這段程式碼 發現基本不會有任何輸出,因為producer 持有鎖後push時發現佇列已滿(最後一行push了一個third event導致佇列滿),consumer又無法獲取 lock,程式就會阻塞。
看下執行緒堆疊:

"Thread-0" prio=6 tid=0x000000000dbe8000 nid=0x1dda0 waiting for monitor entry [0x000000000e6ef000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at ibecase.DeadLockTest$ConsumerThread.run(DeadLockTest.java:26)
    - waiting to lock <0x00000007d5e50ef8> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:722)

"Thread-2" prio=6 tid=0x000000000dbe3000 nid=0x1dd38 waiting on condition [0x000000000e58f000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000007d5e51418> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
    at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:324)
    at ibecase.DeadLockTest$FlushThread.run(DeadLockTest.java:41)
    - locked <0x00000007d5e50ef8> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:722)

Thread-2 持有0x00000007d5e50ef8 並且wait,Thread-0 等待0x00000007d5e50ef8, 由於佇列滿所以Thread-2 將一直WAITING。
如果我們把最後一行程式碼改成

synchronized (lock) {
            queue.put("third event");
        }

我們發現程式大多數情況下又能執行正常。
其實這裡出現的原因就是push 操作與pop操作的額外加鎖 順序不一致導致。pop時沒有lock ,push 有的執行緒有lock有的又沒有加。當然,這裡我們使用的阻塞佇列內部本來就維護了一把鎖,所以上述將lock刪除 即可。
複雜的加鎖順序很容易會使程式出現死鎖,我們在開發中要注意一致的加鎖順序和避免不必要的加鎖。

不要以為我們不會寫出這樣的程式碼,在logstash 1.5版本上就曾出現過這個問題,這裡也是用java程式碼 重新分析了問題原因(logstash 為jruby編寫)。