跳转至

Java Concurrent Modification Exception 全解析

简介

在 Java 编程中,ConcurrentModificationException 是一个常见且令人头疼的异常。当一个线程在遍历集合(如 ArrayListHashMap 等)的同时,另一个线程对该集合进行了结构上的修改(例如添加或删除元素),就可能抛出这个异常。理解这个异常的原理、出现场景以及如何避免它,对于编写健壮的多线程 Java 程序至关重要。

目录

  1. 基础概念
  2. 使用方法(其实不存在真正意义的使用,而是如何处理)
  3. 常见实践(出现场景)
  4. 最佳实践(如何避免)
  5. 小结
  6. 参考资料

基础概念

ConcurrentModificationException 是一个运行时异常(RuntimeException 的子类)。它主要在集合遍历过程中,检测到集合结构发生了意外的改变时抛出。

在 Java 的集合框架中,很多类(如 ArrayListHashMap)不是线程安全的。当多个线程同时访问和修改这些集合时,就可能导致数据不一致或出现 ConcurrentModificationException

例如,考虑以下简单的代码:

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExample {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        // 主线程遍历列表
        for (Integer num : list) {
            System.out.println(num);
            // 尝试在遍历过程中删除元素
            list.remove(num);
        }
    }
}

运行上述代码,会抛出 ConcurrentModificationException。这是因为在使用增强的 for 循环(for-each)遍历集合时,实际上是使用了集合的迭代器(Iterator)。而在遍历过程中调用 list.remove(num) 方法修改了集合的结构,导致迭代器检测到了这种不一致,从而抛出异常。

使用方法(如何处理)

通常情况下,我们不会主动去“使用” ConcurrentModificationException,而是要处理它以确保程序的稳定性。

捕获异常

一种简单的处理方式是捕获异常:

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationHandling {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        try {
            for (Integer num : list) {
                System.out.println(num);
                list.remove(num);
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("捕获到 ConcurrentModificationException: " + e.getMessage());
        }
    }
}

这种方式虽然能防止程序因异常而崩溃,但并不能真正解决问题,只是在异常发生时进行了某种处理。

使用正确的迭代方式

更好的方法是使用迭代器的 remove 方法来删除元素,这样可以避免异常:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ProperIteration {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer num = iterator.next();
            System.out.println(num);
            iterator.remove();
        }
    }
}

在上述代码中,使用 Iteratorremove 方法,它会正确地更新集合的内部状态,从而避免 ConcurrentModificationException

常见实践(出现场景)

多线程环境下的集合操作

在多线程程序中,多个线程同时访问和修改一个非线程安全的集合时,很容易出现 ConcurrentModificationException

import java.util.ArrayList;
import java.util.List;

public class MultiThreadConcurrentModification {
    private static List<Integer> sharedList = new ArrayList<>();

    public static void main(String[] args) {
        sharedList.add(1);
        sharedList.add(2);
        sharedList.add(3);

        Thread thread1 = new Thread(() -> {
            for (Integer num : sharedList) {
                System.out.println("线程1: " + num);
            }
        });

        Thread thread2 = new Thread(() -> {
            sharedList.remove(2);
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,thread1 遍历 sharedList,而 thread2 同时尝试删除 sharedList 中的元素,这很可能导致 ConcurrentModificationException

递归遍历与修改

在递归方法中,如果在遍历集合的过程中进行结构修改,也可能触发此异常。

import java.util.ArrayList;
import java.util.List;

public class RecursiveConcurrentModification {
    private static List<Integer> list = new ArrayList<>();

    static {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public static void recursiveTraversal(int index) {
        if (index >= list.size()) {
            return;
        }
        Integer num = list.get(index);
        System.out.println(num);
        list.remove(num);
        recursiveTraversal(index);
    }

    public static void main(String[] args) {
        recursiveTraversal(0);
    }
}

上述递归方法在遍历列表的同时尝试删除元素,会引发 ConcurrentModificationException

最佳实践(如何避免)

使用线程安全的集合

Java 提供了一些线程安全的集合类,如 CopyOnWriteArrayListConcurrentHashMap

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ThreadSafeCollectionExample {
    public static void main(String[] args) {
        List<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        Thread thread1 = new Thread(() -> {
            for (Integer num : list) {
                System.out.println("线程1: " + num);
            }
        });

        Thread thread2 = new Thread(() -> {
            list.remove(2);
        });

        thread1.start();
        thread2.start();
    }
}

CopyOnWriteArrayList 在进行写操作(如添加或删除元素)时,会创建一个新的数组,读操作则在旧数组上进行,从而避免了并发修改问题。

使用同步机制

可以使用 synchronized 关键字或 ReentrantLock 来同步对集合的访问。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class SynchronizedCollectionExample {
    private static List<Integer> sharedList = new ArrayList<>();
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        sharedList.add(1);
        sharedList.add(2);
        sharedList.add(3);

        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                for (Integer num : sharedList) {
                    System.out.println("线程1: " + num);
                }
            } finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                sharedList.remove(2);
            } finally {
                lock.unlock();
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,通过 ReentrantLock 确保在同一时间只有一个线程可以访问和修改 sharedList,从而避免了 ConcurrentModificationException

小结

ConcurrentModificationException 是 Java 多线程编程中常见的异常,主要由于在集合遍历过程中发生了意外的结构修改导致。为了避免这个异常,我们需要了解集合的线程安全性,正确使用迭代器,并在多线程环境中采取适当的同步机制或使用线程安全的集合类。通过遵循这些最佳实践,可以编写出更健壮、可靠的多线程 Java 程序。

参考资料

  • Oracle Java 文档
  • 《Effective Java》 - Joshua Bloch
  • 《Java Concurrency in Practice》 - Brian Goetz 等