为什么或何时Map.get(..)需要同步?

时间:2013-10-12 17:13:59

标签: java collections concurrency synchronization

这是来自馆藏SynchronizedMap的代码段。我的问题并不是特定于下面的代码片段 - 而是一个通用的:为什么get操作需要同步?

public V get(Object key) {
    synchronized (mutex) {return m.get(key);}
}

4 个答案:

答案 0 :(得分:2)

如果您的线程仅get Map,则不需要同步。在这种情况下,通过使用不可变映射来表达这一事实可能是一个好主意,就像来自Guava库的映射一样,这可以保护您在编译时不会意外地修改映射。

当多个线程正在读取和修改地图时,麻烦就开始了,因为内部结构是Java标准库中的HashMap实现没有为此做好准备。在这种情况下,您可以在该映射周围包装外部序列化层,如

  • 使用synchronized关键字
  • 稍微安全一点就是使用SynchronizedMap,因为这样你就不会忘记synchonized关键字,只需要它,
  • 使用ReadWriteLock保护地图,这将允许多个并发读取线程(这很好)
  • 完全切换到ConcurrentHashMap,准备好让多个线程访问。

但回到原来的问题,为什么首先需要同步:如果不查看类的代码,这有点难以辨别。当一个线程的putremove导致存储桶计数发生变化时,它可能会中断,这会导致读取线程看到太多/太少的元素,因为调整大小尚未完成。也许完全不同的东西,我不知道并且它并不重要,因为它不安全的确切原因可以随时用新的Java版本进行更改。重要的事实是只有 它不受支持,而且你的代码可能会在运行时以一种或另一种方式爆炸。

答案 1 :(得分:0)

如果在调用get()的过程中调整了表的大小,它可能会查找错误的存储桶并错误地返回null。

考虑m.get()中发生的步骤:

  1. 计算密钥的哈希值。
  2. 读取表的当前长度(HashMap中的存储区)。
  3. 此长度用于计算从表中获取的正确存储桶。
  4. 检索存储桶并查看存储桶中的条目,直到找到匹配项或者到达存储桶末尾。
  5. 如果另一个线程更改了地图并导致表格在2&之间调整大小。 3,错误的存储桶可用于查找条目,可能会产生错误的结果。

答案 2 :(得分:0)

并发环境中需要同步的原因是java操作不是原子操作。这意味着像counter++这样的单个java操作会导致底层VM执行多个机器操作。

  1. 读取值
  2. 增值
  3. 写入值
  4. 在执行这三个操作时,可以调用另一个名为 T2 的线程并读取旧值,例如该变量的10 T1 递增该值并将值11写回。但T2已读取值10!在cas中, T2 也应该增加此值,结果保持不变,即11而不是12

    同步将避免此类并发错误。

    <强> T1:

    1. 设置同步器令牌
    2. 读取值
    3. 调用另一个线程T2并尝试读取该值。但由于已经设置了同步器令牌,因此T2必须等待。
    4. 增值
    5. 写入值
    6. 删除同步器令牌
    7. <强> T2:

      1. 设置同步器令牌
      2. 读取值
      3. 增值
      4. 写入值
      5. 删除同步器令牌

答案 3 :(得分:0)

通过同步get方法,您将强制线程穿过内存屏障并从主内存中读取值。如果你不同步get方法,那么JVM采取自由应用底层优化,这可能导致该线程读取幸福地不知道存储在寄存器和缓存中的陈旧值。