2

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

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

4 回答 4

2

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

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

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

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

于 2013-10-12T17:27:37.180 回答
0

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

于 2013-10-12T18:19:00.950 回答
0

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

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

  1. 为密钥计算哈希。
  2. 读取表的当前长度(HashMap 中的桶)。
  3. 此长度用于计算要从表中获取的正确存储桶。
  4. 检索桶并遍历桶中的条目,直到找到匹配项或到达桶的末尾。

如果另一个线程更改了映射并导致表在 2 和 3 之间调整大小,则可能会使用错误的存储桶来查找条目,从而可能给出不正确的结果。

于 2013-10-12T17:31:15.820 回答
0

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

  1. 读取值
  2. 增量值
  3. 写入值

在执行这三个操作的同时,可以调用另一个名为T210的线程并读取例如该变量的旧值。T1增加该值并将该值写11回。但是 T2 有读值10!如果T2也应该增加这个值,结果保持不变,即11代替12.

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

T1:

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

T2:

  1. 设置同步器令牌
  2. 读取值
  3. 增量值
  4. 写入值
  5. 删除同步器令牌
于 2013-10-12T17:41:26.267 回答