每行代码都有潜在的 bug

去年夏天我写了一些代码来实现从一个哈希表中获取一条消息。这条消息是将要通过另外一个线程放入哈希表中的。这里会有很小的概率发生冲突,即一开始查找消息的时候它还没有被保存进去。查找的代码如下:

while ((message = map.get(key)) == null && System.currentTimeMillis() < timeoutTime) {
    wait(1000);
}

wait() 函数调用阻塞当前线程,等待负责向表存入消息的线程调用 notifyAll() 函数。这里 1000 表示 1 秒。大约 5 秒后将会超时。

上面的代码是简单而且正确的。它将会一直保持循环,直到获取得到数值或者超时。超时最多延迟1秒,但在这个案例中不会有问题。(或者说,直到超时发生,你才会遇到更多严重的问题)

代码被另外两个人评审。两个人都抱怨 wait() 函数需要等待”当前”到超时之间的整个时长,而不是仅仅 1 秒。他们认为我的代码不必要地唤醒了线程五次。我的回复是只有在第一秒内是最有可能读取到消息的,而且唤醒一个线程的代价并不大。我认为他们提出的代码会太复杂,而且更有可能存在bug。

他们都说,“一个减法并不复杂”,然后就回到了自己的座位上将他们修改后的版本通过邮件发送给我,想要以此证明他们的办法是多么简单。两个人的代码分别都出现了一个 bug。第一个人的 bug很简单:他使用了错误的常量进行计算。但第二个人的bug却非常微妙:

while ((message = map.get(key)) == null && System.currentTimeMillis() < timeoutTime) {
    wait(timeoutTime - System.currentTimeMillis());
}

这里会存在很小的可能性使得在做减法时,当前时间超过了超时的阈值,从而产生一个负值并传递给了wait(),进而会抛出一个IllegalArgumentException的异常。为了省得计算机一次罕见的线程切换,他引入了一个会偶尔发生并将不可思议地导致运算失败的bug。

(2010年3月15日更新:Ajit Mandalay指出另外一个不好的细节:减法得到0,这意味着“无穷”,循环就有可能永远不会退出)

你写的每一行代码都可能会有一个潜在的bug。所以,除非是当前立刻就需要的或者程序缺了就不能正常运行的,请不要写任何代码。不要推测性地写例程。如果不是立刻需要,就不要写抽象层。如果一个优化会增加任何的复杂性,哪怕是一个减法,也请抵制它。否则五年后,当你的代码中充满有可能是错误的而又从未真正需要的代码时,你会非常遗憾后悔的。

本文文字及图片出自 伯乐在线

余下全文(1/3)
分享这篇文章:

请关注我们:

共有 1 条讨论

  1. 还是作者同事的写法更好,只是需要先把当前时间保存起来,供wait方法调用。这样就不会使得wait被调用时又一次读取新的当前时间,从而导致负数或0。
    long curTimeMillis = System.currentTimeMillis();
    while ((message = map.get(key)) == null && curTimeMillis < timeoutTime) {
    wait(timeoutTime – curTimeMillis);
    }

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注