访问地图给出 java.util.ConcurrentModificationException 尽管地图是使用 ReentrantReadWriteLock 更新的

Accessing map gives java.util.ConcurrentModificationException although map is updated using ReentrantReadWriteLock

我们有一个 spring 引导服务,它只提供来自地图的数据。地图定期更新,由调度程序触发,这意味着我们构建一个新的中间地图加载所有需要的数据,并在完成后立即分配它。为了克服并发问题,我们引入了 Reent运行tReadWriteLock,它会在中间映射发生分配时打开写锁,当然还会在访问映射时打开读锁。请参阅下面的简化代码

@Service
public class MyService {

  private final Lock readLock;
  private final Lock writeLock;

  private Map<String, SomeObject> myMap = new HashMap<>();

  public MyService() {
    final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    readLock = rwLock.readLock();
    writeLock = rwLock.writeLock();
  }

  protected SomeObject getSomeObject(String key) {
    readLock.lock();
    try {
        return this.myMap.get(key);
      }
    } finally {
      readLock.unlock();
    }
    return null;
  }

  private void loadData() {

    Map<String, SomeObject> intermediateMyMap = new HashMap<>();

    // Now do some heavy processing till the new data is loaded to intermediateMyMap

    //clear maps
    writeLock.lock();
    try {
      myMap = intermediateMyMap;
    } finally {
      writeLock.unlock();
    }
  }
}

如果我们将服务设置为负载访问地图,我们仍然会在日志中看到 java.util.ConcurrentModificationException 发生,但我不知道为什么。

顺便说一句:同时我也看到了这个question,这似乎也是一个解决方案。不过,我想知道我做错了什么,或者我是否误解了 Reent运行tReadWriteLock

的概念

编辑:今天我得到了完整的堆栈跟踪。正如你们中的一些人所争论的那样,这个问题实际上与这段代码无关,它只是在重新加载发生的同时巧合地发生了。 问题实际上出在对 getSomeObject() 的访问中。在实际代码中,SomeObject 又是一个 Map 并且这个内部 List 每次被访问时都会被排序(无论如何这都很糟糕,但这是另一个问题)。所以基本上我们 运行 变成 this issue

我看不出代码有什么明显的错误。 ReadWriteLock 应提供必要的内存排序保证(参见 内存同步 部分 https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Lock.html

问题很可能出在 "heavy processing" 部分。 ConcurrentModificationException 也可能是由于在从单个线程迭代地图时修改地图引起的,但是无论系统负载如何,您都会看到同样的问题。

正如您已经提到的,对于这种替换整个地图的模式,我认为 volatile 字段或 AtomicReference 将是更好、更简单的解决方案。

ReentrantReadWriteLock 仅保证在地图上持有锁的线程可以在需要时持有锁。

不保证myMap没有被后台缓存。

缓存的值可能会导致读取过时。

陈旧的阅读会给你 java.util.ConcurrentModificationException

需要声明 myMap volatile 以使更新对其他线程可见。

来自 Java 并发实践:

volatile variables, to ensure that updates to a variable are propagated predictably to other threads. When a field is declared volatile, the compiler and runtime are put on notice that this variable is shared and that operations on it should not be reordered with other memory operations. Volatile variables are not cached in registers or in caches where they are hidden from other processors, so a read of a volatile variable always returns the most recent write by any thread.

Peierls, Tim. Java Concurrency in Practice

另一种方法是在 getSomeObject 上使用 syncronized 并在 myMap = intermediateMyMap;

附近的 this 上使用同步块