【问题标题】:How to make updating BigDecimal within ConcurrentHashMap thread safe如何在 ConcurrentHashMap 线程中更新 BigDecimal 是安全的
【发布时间】:2011-12-19 21:08:37
【问题描述】:

我正在制作一个应用程序,它需要一堆日记帐分录并计算总和。

当有 多个线程 调用 addToSum() 方法时,以下方法是线程/并发安全的。我想确保每次调用都能正确更新总数。

如果不安全,请说明我必须做些什么来确保线程安全。

我需要synchronize get/put 还是有更好的方法?

private ConcurrentHashMap<String, BigDecimal> sumByAccount;

public void addToSum(String account, BigDecimal amount){
    BigDecimal newSum = sumByAccount.get(account).add(amount);
    sumByAccount.put(account, newSum);
}

非常感谢!

更新:

感谢大家的回答,我已经知道上面的代码不是线程安全的

感谢 Vint 建议使用 AtomicReference 替代 synchronize。我之前使用AtomicInteger 来保存整数和,我想知道 BigDecimal 是否有类似的东西。

两者的优劣是否有定论?

【问题讨论】:

    标签: java concurrency thread-safety bigdecimal concurrent-collections


    【解决方案1】:

    您可以像其他人建议的那样使用同步,但如果想要一个最小阻塞的解决方案,您可以尝试 AtomicReference 作为 BigDecimal 的存储

    ConcurrentHashMap<String,AtomicReference<BigDecimal>> map;
    
    public void addToSum(String account, BigDecimal amount) {
        AtomicReference<BigDecimal> newSum = map.get(account);
        for (;;) {
           BigDecimal oldVal = newSum.get();
           if (newSum.compareAndSet(oldVal, oldVal.add(amount)))
                return;
        }
    }
    

    编辑 - 我将对此进行更多解释:

    AtomicReference 使用CAS 以原子方式分配单个引用。循环说明了这一点。

    如果存储在 AtomicReference 中的当前字段 == oldVal [它们在内存中的位置,而不是它们的值],则将存储在 AtomicReference 中的字段的值替换为 oldVal.add(amount)。现在,在你调用 newSum.get() 的 for 循环之后的任何时候,它都会有已添加到的 BigDecimal 对象。

    您想在此处使用循环,因为可能有两个线程试图添加到同一个 AtomicReference。一个线程成功而另一个线程失败的情况可能会发生,如果发生这种情况,只需使用新的附加值重试。

    对于适度的线程竞争,这将是一个更快的实现,对于高竞争,您最好使用synchronized

    【讨论】:

    • 好主意,但我认为您有一些需要纠正的地方:您在条件检查中调用了两次newSum.get(),但它可能会在两次调用之间发生变化。您也许应该使用do { } while(),您只调用一次get
    • 感谢您的解释。 +1。
    • 根据 Java 语言规范,“建议代码不要过分依赖”“运算符的操作数似乎以特定的求值顺序,即从左到右。”因此,尽管我相信您的代码是合理的,但我认为编写 while(true) { BigDecimal oldVal = newSum.get(); BigDecimal newVal = oldVal.add(amount); if(newSum.compareAndSet(oldVal, newVal)) break; } 之类的内容可能会更好。
    • 您确定synchronized 更适合高争用吗?我认为(没有特别的原因)CAS 总是更好。
    • @toto2:由于右边的调用发生在左边的调用之后,因此两个调用之间的任何更改都会导致compareAndSet 什么都不做并返回false。所以我相信技术上没问题;但无论如何我同意你的看法,最好只调用一次get,这样代码更容易推理。
    【解决方案2】:

    您的解决方案不是线程安全的。原因是总和可能会丢失,因为要放置的操作与要获取的操作是分开的(因此您放入映射中的新值可能会丢失同时添加的总和)。

    做你想做的最安全的方法是同步你的方法。

    【讨论】:

      【解决方案3】:

      这是安全的,因为线程 A 和 B 可能同时调用sumByAccount.get(account)(或多或少),所以谁都不会看到对方add(amount) 的结果。也就是说,事情可能会按以下顺序发生:

      • 线程 A 调用 sumByAccount.get("accountX") 并获得(例如)10.0。
      • 线程 B 调用 sumByAccount.get("accountX") 并获得与线程 A 相同的值:10.0。
      • 线程 A 将其 newSum 设置为(例如)10.0 + 2.0 = 12.0。
      • 线程 B 将其 newSum 设置为(例如)10.0 + 5.0 = 15.0。
      • 线程A调用sumByAccount.put("accountX", 12.0)
      • 线程 B 调用 sumByAccount.put("accountX", 15.0),覆盖线程 A 所做的事情。

      解决此问题的一种方法是将synchronized 放在您的addToSum 方法中,或者将其内容包装在synchronized(this)synchronized(sumByAccount) 中。另一种方式是,由于上述事件序列仅在两个线程同时更新同一个帐户时才会发生,因此可能是基于某种Account 对象进行外部同步。没有看到你的程序逻辑的其余部分,我不能确定。

      【讨论】:

        【解决方案4】:

        是的,您需要同步,否则您可以让两个线程各自获得相同的值(对于相同的键),例如 A 和线程 1 将 B 添加到它,线程 2 将 C 添加到它并将其存储回来。现在的结果将不是 A+B+C,而是 A+B 或 A+C。

        您需要做的是锁定添加的共同点。除非您这样做,否则在 get/put 上同步将无济于事

        synchronize {
            get
            add
            put
        }
        

        但是如果你这样做了,那么你将阻止线程更新值,即使它是针对不同的键。您想在帐户上同步。但是,在字符串上同步似乎不安全,因为它可能导致死锁(您不知道还有什么会锁定字符串)。您可以创建一个帐户对象并将其用于锁定吗?

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-07-16
          • 1970-01-01
          • 2012-08-20
          • 1970-01-01
          • 2019-09-22
          • 2014-03-04
          • 1970-01-01
          • 2018-01-31
          相关资源
          最近更新 更多