【问题标题】:ConcurrentHashMap Conditional ReplaceConcurrentHashMap 条件替换
【发布时间】:2009-09-08 16:44:02
【问题描述】:

我希望能够有条件地替换 ConcurrentHashMap 中的值。也就是说,给定:

公共类 PriceTick { 最终字符串工具ID; ... 最终长时间戳; ...

还有一个类(我们称之为 TickHolder),它拥有一个 ConcurrentHashMap(我们称之为 map)。

我希望能够实现一个有条件的 put 方法,这样如果键没有条目,则插入新的条目,但如果存在现有条目,则插入新的条目仅当新 PriceTick 中的时间戳值大于现有的值

对于老式的 HashMap 解决方案,TickHolder 会有一个 put 方法:

公共无效添加(PriceTick 刻度){ 同步(地图){ 如果 ((map.get(tick.instrumentId) == null) || (tick.getTimestamp() > map.get(tick.instrumentId).getTimestamp()) map.put(tick.instrumentId,tick); } }

对于 ConcurrentHashMap,人们会想要放弃同步并使用一些原子方法,如替换,但这是无条件的。所以很明显必须写“条件替换”方法。

然而,由于 test-and-replace 操作是非原子的,为了线程安全,它必须是同步的 - 但我最初阅读 ConcurrentHashMap 源代码让我认为外部同步和它们的内部同步锁不会很好地工作,所以至少,每个执行结构更改和包含类执行的 Map 方法都必须由包含类同步......即使那样,我也会相当不安。

我考虑过继承 ConcurrentHashMap,但这似乎是不可能的。它使用了一个具有默认访问权限的内部 final 类 HashEntry,因此虽然 ConcurrentHashMap 不是 final,但它是不可扩展的。

这似乎意味着我必须回退到将 TickHolder 实现为包含一个老式 HashMap 才能编写我的条件替换方法。

那么,问题是:我对上述内容是否正确?我是否(希望)遗漏了一些东西,无论是明显的还是微妙的,都会导致不同的结论?我真的很想在这里使用那个可爱的条纹锁定机制。

【问题讨论】:

    标签: java multithreading collections


    【解决方案1】:

    非确定性解决方案是循环replace()

    do {
      PriceTick oldTick = map.get(newTick.getInstrumentId());
    } while ((oldTick == null || oldTick.before(newTick)) && !map.replace(newTick.getInstrumentId(), oldTick, newTick);
    

    虽然看起来很奇怪,但这是此类事情的普遍建议模式。

    【讨论】:

    • 有趣。当您说“不确定”时,您是说结果是不确定的吗?或者说时间是什么?另外,您的“之前”方法是为了代表我的时间戳比较,我说得对吗?还是我错过了什么?
    • 非确定性是指可能需要多次迭代才能完成替换,因为 get 和 replace 显然不是原子操作,但它是安全的。
    • 知道了。谢谢。来自非确定性方法的确定性结果(显然具有非确定性时间)。让我想起了 TCP/IP。 :) 谢谢,好主意。
    • Replace 不允许 oldTick 为 null,因此当尝试将 oldTick==null 替换为 newTick 时,此解决方案应直接通过 replace 进入 NullPointerException...
    【解决方案2】:

    @cletus 解决方案为我解决几乎相同的问题奠定了基础。我认为需要进行一些更改,尽管 oldTick 为 null 然后 replace 抛出 NullPointerException,如 @hotzen 所述

    PriceTick oldTick;
    do {
      oldTick = map.putIfAbsent(newTick.getInstrumentId());
    } while (oldTick != null && oldTick.before(newTick) && !map.replace(newTick.getInstrumentId(), oldTick, newTick);
    

    【讨论】:

    • 你试过cletus的解决方案了吗?因为我不明白你为什么会有NullPointerException。该解决方案使用||&&,因此如果oldTick 为null,则不会执行两个表达式的每个右侧,从而防止任何NullPointerException
    • @Marc-Andre Rob 是正确的。使用 Cletus 编写的解决方案,第一次调用 add 将导致 NullPointerException。当我第一次看到答案时遇到了这个问题,在我的项目中进行了更改,但从未修改过这个帖子。
    • @CPerkins 感谢您的更新!我不太明白出了什么问题,但我也没有测试过代码。
    • @Marc-Andre 如果 oldTick 为空,则满足(oldTick == null || oldTick.before(newTick))oldTick == null 一侧,因此我们继续穿过&& 到map.replace,它显式检查并抛出一个如果 oldTick 或 newTick 为 null,则出现 NullPointerException。而且由于我们不应该在 cmets 中进行对话,所以我现在停止。
    • ((oldTick == null || oldTick.before(newTick)) && !map.replace()) 如果oldTick == null 我们有((true) && !map.replace()。通常,如果您在&& 的左侧有 true,它不应该在右侧执行(或者我需要一些睡眠,因为我忘记了一些基础知识)。但正如你所说,我们不应该聊天,我会自己做一些测试。 (我可能只是忽略了替换,因为我从未使用过它)
    【解决方案3】:

    正确答案应该是

    PriceTick oldTick;
    do {
      oldTick = map.putIfAbsent(newTick.getInstrumentId(), newTick);
      if (oldTick == null) {
        break;
      }
    } while (oldTick.before(newTick) && !map.replace(newTick.getInstrumentId(), oldTick, newTick);
    

    【讨论】:

      【解决方案4】:

      作为替代方案,您能否创建一个 TickHolder 类,并将其用作地图中的值?它使地图使用起来稍微麻烦一些(获取值现在是 map.getValue(key).getTick()),但它可以让您保持 ConcurrentHashMap 的行为。

      public class TickHolder {
        public PriceTick getTick() { /* returns current value */
        public synchronized PriceTick replaceIfNewer (PriceTick pCandidate) { /* does your check */ }
      }
      

      你的 put 方法变成这样:

      public void updateTick (PriceTick pTick) {
        TickHolder value = map.getValue(pTick.getInstrumentId());
        if (value != null) {
          TickHolder newTick = new TickHolder(pTick);
          value = map.putIfAbsent(pTick.getInstrumentId(), newTick);
          if (value == null) {
            value = newTick;
          }
        }
        value.replaceIfNewer(pTick);
      }
      

      【讨论】:

      • 有趣。您是要同步 PriceTick 吗?这种同步如何与 ConcurentHashMap 中的锁定机制一起发挥作用?
      • TickHolder.replaceIfNewer 需要同步 - 这基本上是您的原子测试和条件更新操作的所在。 TickHolder.replaceIfNewer 上的同步与地图的同步之间没有直接的交互。地图(在 updateTick 中有一点逻辑)确保给定键的 TickHolder 不会超过一个; replaceIfNewer 同步确保对给定 TickHolder 的更新没有线程问题。 updateTick 和 PriceTick 本身(假设 PriceTick 是不可变的)都不需要任何其他同步
      • 我有点困惑,为什么您认为不需要其他同步。 replaceIfNewer 的同步只限制对该方法的调用。所以其他线程可以使用其他方法对地图进行结构更改,不是吗?不过,这是一种有趣的方法。以后会考虑的。
      • 我想我在做一个隐含的假设,即不会从您的地图中删除任何内容 - 所有写入都通过 updateTick。在这种情况下,一旦您获得了给定 InstrumentId 的 TickHolder,您就不再关心地图的其他结构更改;您拥有的 TickHolder 是 Map 中 InstrumentId 的值,您只需要将您的操作与该 InstrumentId 上的其他操作互斥即可。
      • 蜱确实必须被删除,虽然我没有这么说。但请注意,这不仅仅是删除 - 添加也可能导致结构变化(例如,重新存储)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-24
      • 2023-04-09
      • 2018-07-10
      • 1970-01-01
      • 2020-04-03
      相关资源
      最近更新 更多