【问题标题】:SynchronizedMap ConcurrentModificationExceptionSynchronizedMap ConcurrentModificationException
【发布时间】:2018-01-18 14:15:39
【问题描述】:

我正在尝试理解 SynchronizedMap 并运行了以下代码。我得到以下输出,但有一个例外。根据我的理解,当线程仍在对映射执行写入或线程处于睡眠状态时,get() 方法尝试访问同步映射时会导致异常。我的理解是正确的还是我遗漏了什么?

class MapHelper1 implements Runnable {
Map<String, Integer> map;

public MapHelper1(Map<String, Integer> map) {
    this.map = map;
    new Thread(this, "MapHelper1").start();
}

public void run() {
    map.put("One", 1);
    try {
        System.out.println("MapHelper1 sleeping");
        Thread.sleep(100);
    } catch (Exception e) {
        System.out.println(e);
    }

}
}

class MapHelper2 implements Runnable { 
Map<String, Integer> map;

public MapHelper2(Map<String, Integer> map) {
    this.map = map;
    new Thread(this, "MapHelper3").start();
}

public void run() {
    map.put("two", 1);
    try {
        System.out.println("MapHelper2 sleeping");
        Thread.sleep(100);
    } catch (Exception e) {
        System.out.println(e);
    }

}
}


class MapHelper3 implements Runnable {
Map<String, Integer> map;

public MapHelper3(Map<String, Integer> map) {
    this.map = map;
    new Thread(this, "MapHelper3").start();
}

public void run() {
    map.put("three", 1);
    try {
        System.out.println("MapHelper3 sleeping");
        Thread.sleep(100);
    } catch (Exception e) {
        System.out.println(e);
    }

}
}

public class MainClass {

public static void main(String[] args) {
    Map<String, Integer> hashMap = new HashMap<>();
    Map<String, Integer> syncMap = Collections.synchronizedMap(hashMap);
    MapHelper1 mapHelper1 = new MapHelper1(syncMap);
    MapHelper2 mapHelper2 = new MapHelper2(syncMap);
    MapHelper3 mapHelper3 = new MapHelper3(syncMap);



    for (Map.Entry<String, Integer> e : syncMap.entrySet()) {
        System.out.println(e.getKey() + "=" + e.getValue());
    }

}

}

输出:

MapHelper1 sleeping
MapHelper2 sleeping
MapHelper3 sleeping


Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1494)
at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1527)
at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1525)
at MainClass.main(MainClass.java:137)
Command exited with non-zero status 1

编辑:当输出无异常生成时,我再次运行代码几次。为什么会出现这种行为?

【问题讨论】:

  • 这不是一个通用的地图迭代问题,而是一个Collections.synchronizedMap 问题。这是有效的。并回答:您必须在 syncMap 外部迭代上手动同步。查看synchronizedMap方法javadoc
  • @Kayaman 我知道您链接到的问题是相关的,但是在这个问题中删除了哪些条目?请重新考虑打开这个问题,或者找到另一个真正的重复项
  • 好的,我会重新打开这个。那你最好把一些好的答案放在那里。
  • @ghostrider 不,因为在 3 个 put 之间存在竞争条件,使迭代器超过了条目集。如果所有 3 个 put 在获取迭代器之前发生,那么它会运行正常,但你不能相信这总是会以这种方式发生
  • @ghostrider 是的,尽管读取线程可能会在一个写入操作和另一个写入操作之间启动,即没有保证顺序。同步映射对于读/写“原子”操作很有用。遍历整个地图不是“原子”读取操作,而是“批量”读取操作(以某种方式命名)。底层迭代器一次“原子地”获取一个条目。从这个意义上说,地图是同步的,但不是整个遍历。

标签: java multithreading concurrency synchronization


【解决方案1】:

迭代时不同步访问。使用:

synchronized(syncMap) {
  for (Map.Entry<String, Integer> e : syncMap.entrySet()) {
    System.out.println(e.getKey() + "=" + e.getValue());
  }
}

甚至在synchronizedMap()方法javadoc中

用户必须手动同步返回的 迭代其任何集合视图时的映射:

  Map m = Collections.synchronizedMap(new HashMap());
      ...
  Set s = m.keySet();  // Needn't be in synchronized block
      ...
  synchronized (m) {  // Synchronizing on m, not s!
      Iterator i = s.iterator(); // Must be in synchronized block
      while (i.hasNext())
          foo(i.next());
  }

【讨论】:

  • 我又跑了几次代码,无一例外的生成了输出。为什么会出现这种行为?
  • 同步错误通常是不可重现的。每次运行都会得到不同的结果。没有人知道该错误是否会在下一次运行中再次出现。 @ghostrider
  • @OleV.V.谢谢,是的,我现在明白了.. 那么同步地图在读取和写入同一张地图方面有什么作用。在这个问题中,线程是否获取整个映射的锁,并且仅在 Thread.sleep() 完成执行时才释放?在这整个期间,读取线程(如果有的话)是否必须等待最终编辑?
【解决方案2】:

我刚刚阅读了@Dariusz 的回答,我认为这是完全正确的。

要具体回答您看到随机行为的原因,完全是时机。也就是说,如果在主线程中迭代之前所有三个线程都填充了映射,那么一切都很好。但是,如果一个线程在主线程中已经在进行迭代时尝试填充,则会出现异常。

顺便说一句,我知道这是为了演示目的。但在实际代码中,最好不要在构造函数中启动线程。

【讨论】:

  • 顺便说一句,我回答而不是发表评论,因为我还没有评论的声誉。
  • 谢谢,是的,我现在明白了.. 那么同步地图在读取和写入同一张地图方面有什么作用。在这个问题中,线程是否获取整个映射的锁,并且仅在 Thread.sleep() 完成执行时才释放?在这整个期间,读取线程(如果有的话)是否必须等待最终编辑?
  • 1 在“put”调用返回后立即释放锁
  • 2 所有线程都需要锁在同一个锁上,在这种情况下就是syncMap本身; 3 syncMap.entrySet() 调用实际上获取然后释放相同的锁(syncMap 本身),但是底层的map 条目数据仍然是共享的,因此ConcurrentModification Exception。事实上,可以在单线程中得到这样的异常,因为根本原因是针对迭代开始时拍摄的快照进行迭代检查。 4、ConcurrentMap 可能不是这里想要的,而是你需要时的另一个工具。
  • 附加说明应该会有所帮助。即使在主线程中同步了 syncMap,程序仍然可以根据时间显示随机结果。 ConcurrentModificationException 肯定会消失,但它可以打印 0、1、2 等条目等。如果主线程需要在所有工作人员完成写入后收集数据,那么您需要线程同步器。在这种情况下,单独的数据访问锁不会这样做。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-06
  • 2023-03-03
相关资源
最近更新 更多