之所以说是类死锁,因为发生的现象几乎与死锁相同,程序将一直阻塞下去,但是又没有形成环路。
本次介绍案例中,是阻塞队列引起的。
阻塞队列有一个特点:
队列满时, 往队列放入元素会被阻塞;
队列空时, 从队列取出元素会被阻塞。
假设有一个共享阻塞队列,和一把锁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编写)。