一個由阻塞佇列引發的類死鎖案例
之所以說是類死鎖,因為發生的現象幾乎與死鎖相同,程式將一直阻塞下去,但是又沒有形成環路。
本次介紹案例中,是阻塞佇列引起的。
阻塞佇列有一個特點:
佇列滿時, 往佇列放入元素會被阻塞;
佇列空時, 從佇列取出元素會被阻塞。
假設有一個共享阻塞佇列,和一把鎖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編寫)。