Java的“Copy-on-Iterate”习惯用法也不安全
这是我们的天才Lauri Tulmin处理的一个有趣的技术支持的故事。问题看起来是Wicket里的JRebel导致的ArrayIndexOutOfBoundsException
异常,很罕见。经过一些分析调查,他发现这个异常最先是由下面的Wicket代码抛出的:
private final Map<IModifiable, Entry> modifiableToEntry =
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拷贝出来使用,就像下面这样:
for(Iterator i = new ArrayList(collection).iterator(); i.hasNext();) {...}
为了使collection能在多线程的环境中使用,必须保证它的可同步性和其它相关的特性。
这种用法非常普遍,只要在Google Code 里简单搜一下就能证明。事实上我们在JRebel程序里多次的这样使用过,在Wicket里的很多地方也是这样用的。所以这怎么会出现ArrayIndexOutOfBoundsException
异常?
Lauri经过深入的研究发现,这种写法在多线程环境中有天生的缺陷(即使在collection已经被同步锁的情况下!)。原因就在于Java 1.6之前的ArrayList
的构造方式。在我的1.5版Java SDK源代码里它是这样写的:
public ArrayList(Collection<? extends E> collection) {
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()
方法:
for (Iterator i = Arrays.asList(collection.toArray()).iterator(); i.hasNext();)
【英文出处】:Link