一个 ArrayBlockingQueue 不当使用,导致公司损失几百万!

/ Java / 没有评论 / 1304浏览

一个 ArrayBlockingQueue 不当使用,导致公司损失几百万!

我们为什么要招高级程序员呢?因为高级程序员写的 bug 可能更少,在调用 api 的时候,犯错的概率更小。但是并不意味这高级程序员就不犯错。今天我们就一起来分享一个由于 ArrayBlockingQueue 使用不当,导致公司损失几百万的案例!

根据 ArrayBlockingQueue 的名字我们都可以看出,它是一个队列,并且是一个基于数组的阻塞队列。

ArrayBlockingQueue 是一个有界队列,有界也就意味着,它不能够存储无限多数量的对象。所以在创建 ArrayBlockingQueue 时,必须要给它指定一个队列的大小。

我们先来熟悉一下 ArrayBlockingQueue 中的几个重要的方法。

我们再来看一下 ArrayBlockingQueue 使用场景。

ArrayBlockingQueue 进队操作采用了加锁的方式保证并发安全。源代码里面有一个 while() 判断:

public void put(E e) throws InterruptedException {
    checkNotNull(e); // 非空判断
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 获取锁
    try {
        while (count == items.length) {
            // 一直阻塞,知道队列非满时,被唤醒
            notFull.await();
        }
        enqueue(e); // 进队
    } finally {
        lock.unlock();
    }
}
public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    checkNotNull(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
        // 阻塞,知道队列不满
        // 或者超时时间已过,返回false
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}

通过源码分析,我们可以发现下面的规律:

出队的源码类似,我就不贴了。ArrayBlockingQueue 队列我们可以在创建线程池时进行使用。

new ThreadPoolExecutor(1, 1,
  0L, TimeUnit.MILLISECONDS,
  new ArrayBlockingQueue<Runnable>(2));

new ThreadPoolExecutor(1, 1,
  0L, TimeUnit.MILLISECONDS,
  new LinkedBlockingQueue<Runnable>(2));  

了解了这些后,再来看看我们开发人员的使用。

当时线上系统出故障后,导致所有的请求都处理不了。给人的感觉就是,界面上一直在转圈。

于是不得不 dump 线程,然后重启机器,先恢复使用。每次一故障,客服电话就被打爆了,投诉率疯升,当天订单大幅下滑。前前后后发生几次故障,领导都气疯了。几百万就这样没了,所以给我们的压力非常的大。

dump 下来后,我分析发现线程都 Block 在写日志的地方。然后,我前前后后怕查,发现了 block 在了 ArrayBlockingQueue.put 这个方法。检查源码,发现创建了 ArrayBlockingQueue(250) 个长度的队列。当队列超过 250 时,put 就一定会被 block 住。

业务代码抽象如下:

if (blockingQueue.remainingCapacity() < 1) { 
    //todo 
} 
blockingQueue.put(...) 

这里两个悲催的问题,一是这个 if 判断完后,还是会进行 put 操作,应该是 else 中进行 put 操作;二是满了之后,还在 todo,做其他事情。

其实我们这里可以完全没必要进行 if (blockingQueue.remainingCapacity() < 1) 判断,使用 blockingQueue.offer 不就完事了嘛。如果 BlockingQueue 可以容纳,则返回 true,否则返回 false。

所以说,除了技术本身外,代码的细节功力是非常非常重要的。