这是每个人在使用wait时第一次遇到的陷阱。清单A展示了如何对一个随意的对象发出wait调用。在wait调用之前,代码能够很好地编译和运行。之后,它会引发一个IllegalMonitorStateException异常。对一个对象调用wait之前,首先要将wait调用封闭到一个同步的块中,从而获得对象的监视器。清单B提供了这样的一个例子。幸运的是,这个陷阱很容易避免;因为有现成的异常可供捕捉。但是,线程处理所牵涉的大多数错误都没有这么“体贴”,它们会间歇性地发生,或者悄悄地死锁。下面来看看几个例子。
我们经常将线程用于一个循环的封送(dispatch)或处理器(processor)线程。这些线程无休止地循环,等待一个外部线程改变可相互访问的一个对象的状态。但是,如果不小心地同步两个线程所访问的对象,就可能不知不觉发生死锁。在清单C中,一个处理器线程不停循环,直到Boolean变量done被设为true。程序作者明显是想让一个外部线程将done标志切换为true,并在当前循环结束之后就退出循环。
然而,Boolean变量done的状态可能不慎越过线程边界。Java内存模型并不保证在一个线程中对done进行的改动会在另一个线程中反映出来。所以,可能有一个外部线程改换了这个标志的状态,而其他线程中的while循环无休止地进行。老练的开发者也许会说,他们以前用的就是像清单C那样的代码,而且都能很好地工作。这并没有错;它通常确实能很好地工作。但是,并不保证它总是像预期的那样工作,而且每个有经验的开发者都应知道适用于重要事件及间歇性错误的“摩非定律”(会出错的终将出错——译注)。
Java内存模型要求:遇到一个同步障碍时,变量状态是正确的。这可消除处理器线程无限循环的风险,如清单D所示。“博学”的Java开发者可能会问,能否用volatile关键字来纠正相同的问题?volatile关键字确实是为这种情况而设的,它的宗旨是:如果变量已在另一个线程中更新,那么在访问过时数据时,就强迫进行内存读取。遗憾的是,Java语言规范在解释volatile的工作原理时语焉不详,许多虚拟机甚至完全忽略了这个关键字。