【问题标题】:Can we use Synchronized for each entry instead of ConcurrentHashMap?我们可以为每个条目使用 Synchronized 而不是 ConcurrentHashMap 吗?
【发布时间】:2016-11-23 01:04:10
【问题描述】:

这就是问题所在:我们想要一个其条目是线程安全的哈希表。

假设我有一个<String, Long> 的哈希表,我想安全地增加其中一个条目线程的值:以下可以吗?:

HashMap<String , Long> hashTable = new HashMap<String, Long>();

然后每当我想增加一个条目时:

Synchronized (hashTable.get("key"))
{
    Long value = hashTable.get("key");
    value++;
    hashTable.put("key", value);
}

我认为它比 ConcurrentHashMap 更好,因为它只锁定一个条目,不像 ConcurrentHashMap 使用存储桶,并将一组条目锁定在一起。

更重要的是,我不知道如何安全地使用 COncurrenHashMap 递增它。比如我认为下面的代码是不正确的:

 ConcurrentHashMap<String , Long> hashTable = new ConcurrentHashMap<String, Long>();


 Long value = hashTable.get("key");
 value++;
 hashTable.put("key", value);

我认为是不正确的,因为两个线程可以一个接一个地读取key,一个接一个地写入,最终得到一个错误的值。

你们怎么看?

【问题讨论】:

  • 你有任何性能数据吗?金钱在这种情况下说话,如果你真的相信你有一些有价值的东西,你应该发布数据,让人们有信心甚至看你的算法。
  • 你建议你有一个在并发环境中比 ConcurrentHashMap 更正确或更高效的 Map 版本。除了检查,你的证据是什么?
  • 不,我问你:可以吗?

标签: java hashmap synchronized


【解决方案1】:

您提出的方法不是线程安全的,因为初始 hashTable.get() 操作(通过它您获取要同步的对象)本身相对于其他线程并不同步 put()ing 关联的值同一把钥匙。此外,您的代码没有考虑将新值添加到地图或从地图中删除键的可能性(所谓的“结构修改”)。如果有可能发生这种情况,无论键是什么,那么这些操作必须与 所有 对地图的其他访问同步。

不过,您说得对,ConcurrentHashMap 也不能解决这些问题。它提供的单个操作是线程安全的,其中包括Map 本身未定义的一些操作,但必须作为不间断单元执行的系列操作仍然需要保护通过同步。

我建议一种稍微不同的方法:使用ConcurrentHashMapAtomicLong,它是可变的,作为您的值类型,而不是Long

ConcurrentHashMap<String, AtomicLong> map;

然后,要更新键的值,即使您不确定该键在映射中是否已经有条目,您也可以这样做:

AtomicLong value = map.putIfAbsent(key, new AtomicLong(0));
long updatedValue = value.incrementAndGet();

putIfAbsent() 确保值对象不会被相互冲突的 put 操作破坏。使用AtomicLong 避免了多个操作联合同步的需要,因为只需要一次映射访问——检索到的值由访问它的所有线程共享,并且本身可以原子更新而无需进一步访问映射。

如果您可以确定映射已经具有给定键的映射,那么您可以简单地这样做:

AtomicLong value = map.get(key);
long updatedValue = value.incrementAndGet();

无论哪种方式,我认为这是您可以为您描述和暗示的操作做的最好的事情。

更新:

您甚至可以考虑像这样结合这两种方法:

AtomicLong value = map.get(key);

if (value == null) {
    value = map.putIfAbsent(key, new AtomicLong(0));
}

long updatedValue = value.incrementAndGet();

假设对于给定的键还没有映射的情况比较少见,并且在这种情况下避免创建新的AtomicLong。如果没有找到映射,则必须第二次访问该映射,以确保存在映射并获取相应的值,但这里我们仍然需要putIfAbsent(),如果我们想避免同步,因为两个线程是可能的两者都尝试在大约同一时间为同一个键添加映射。当需要添加新条目时,成本会更高,但平均而言,它可能会比我的第一个建议成本更低。然而,与任何性能问题一样,测试是必不可少的。

【讨论】:

  • @MohammadRoohitavaf,首先,必须了解与long 原始值不同,Long 对象是不可变的。因此,您不能实际增加Long;如果您尝试,结果是具有所需值的 不同对象。那个不同的对象有它自己的、单独的监视器(锁),并且持有一个监视器的线程不会因此与持有另一个监视器的线程同步。这也是为什么您必须在您的版本中将 put() 更新后的值添加到地图中,否则它将继续保留旧值。
  • @MohammadRoohitavaf,其次,您需要认识到您的版本在hashTable.get() 返回的对象上同步,但在它可以这样做之前,hashTable.get() 必须执行。在该执行过程中,线程读取映射的内部数据结构以查找给定键的条目并提取相关值;如果另一个线程修改了这些内部数据结构中的任何一个,而两个线程之间没有某种形式的同步,那么您的程序没有正确同步。
  • @MohammadRoohitavaf,地图的任何结构修改都可以修改地图的内部状态的一部分,地图上的任何其他操作都会读取。任何具有给定键的put() 都将修改映射内部状态的一部分,具有相同键的get() 将读取这些状态。因此,这些操作需要与您的初始get() 同步,但在您提议的代码中,它们不是。因此,您的代码没有正确同步,也就是说,不是线程安全的。
  • 谢谢,我想我明白了。但是关于 Long 的不变性,我尝试了这个代码: Long l = new Long(0) ; l++; System.out.println(l);它工作正常,打印 1。那么,它不是表明我们可以增加一个 Long 对象吗?
  • @MohammadRoohitavaf,不,您的代码没有显示您可以增加Longl++ 的评估将 Long 拆箱以获得 long,增加 long,自动装箱该值以创建一个新的 不同 Long 实例,最后分配新的Long 到变量 l。原始的Long 保留了它从一开始就有的相同值,您可以验证您是否有另一个对它的引用——当然,这也证明它是一个不同的对象。
猜你喜欢
  • 2012-07-20
  • 2014-04-27
  • 1970-01-01
  • 2022-10-02
  • 1970-01-01
  • 1970-01-01
  • 2012-01-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多