Java的“Copy-on-Iterate”習慣用法也不安全
這是我們的天才Lauri Tulmin處理的一個有趣的技術支持的故事。問題看起來是Wicket里的JRebel導致的ArrayIndexOutOfBoundsException
異常,很罕見。經過一些分析調查,他發現這個異常最先是由下面的Wicket代碼拋出的:
new ConcurrentHashMap();
public void start(Duration pollFrequency) {
this.task = new Task("ModificationWatcher");
this.task.run(pollFrequency, new ICode() {
public void run(Logger log) {
Iterator iter = new ArrayList(
ModificationWatcher.this.modifiableToEntry.values()).iterator();
while (iter.hasNext())
很明確,異常是由ArrayList
構造器拋出的。但這怎么可能?
讓我們先暫停一下,說明一下這段代碼為什么要這樣寫。在使用集合(collections)時有可能會出現一個問題,就是當我們重復迭代這個集合時,如果這個集合不巧被修改了(通常是被另外的線程),程序就會拋出ConcurrentModificationException
異常。這是為了防止Iterator
上的不可預期的操作行為。為了避免這個問題,有一個共識的習慣用法,就是在迭代循環之前要把collection拷貝出來使用,就像下面這樣:
為了使collection能在多線程的環境中使用,必須保證它的可同步性和其它相關的特性。
這種用法非常普遍,只要在Google Code 里簡單搜一下就能證明。事實上我們在JRebel程序里多次的這樣使用過,在Wicket里的很多地方也是這樣用的。所以這怎么會出現ArrayIndexOutOfBoundsException
異常?
Lauri經過深入的研究發現,這種寫法在多線程環境中有天生的缺陷(即使在collection已經被同步鎖的情況下!)。原因就在于Java 1.6之前的ArrayList
的構造方式。在我的1.5版Java SDK源代碼里它是這樣寫的:
int size = collection.size();
firstIndex = 0;
array = newElementArray(size + (size / 10));
collection.toArray(array);
lastIndex = size;
modCount = 1;
}
問題就出在size
被記錄的時間和collection.toArray(array)被調用
的時間有個競爭關系。就在這個時間差內,理論上(的確是有可能)collection的size被其它線程修改了。當size變大了,用toArray()
拷貝array時就會出錯,出現可怕的ArrayIndexOutOfBoundsException
異常。在經過進一步研究后我們發現在Oracle Java 1.6 和之后的版本里就不會出現這個問題了。可是我卻沒有找到跟這個問題相關的bug聲明,看來它只是被意外的被修復了。
那么如何才能避免這個問題?一個辦法是在整個循環上加上同步鎖,但這就會限制你只能當和其它線程在同一個同步區內才能訪問這個集合。有一個簡單的解決方案,就是使用像下面這樣使用 toArray()
方法:
【英文出處】:Link