一个由阻塞队列引发的类死锁案例

之所以说是类死锁,因为发生的现象几乎与死锁相同,程序将一直阻塞下去,但是又没有形成环路。

本次介绍案例中,是阻塞队列引起的。
阻塞队列有一个特点:
队列满时, 往队列放入元素会被阻塞;
队列空时, 从队列取出元素会被阻塞。

假设有一个共享阻塞队列,和一把锁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编写)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值