16

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

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

如果不安全,请解释我必须做什么来确保线程安全。

我需要synchronize获取/放置还是有更好的方法?

private ConcurrentHashMap<String, BigDecimal> sumByAccount;

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

非常感谢!

更新:

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

感谢 Vint 建议AtomicReferencesynchronize. 我以前AtomicInteger用来保存整数和,我想知道 BigDecimal 是否有类似的东西。

关于两者的利弊,是否有明确的结论?

4

4 回答 4

18

您可以像其他人建议的那样使用同步,但如果想要一个最小阻塞的解决方案,您可以尝试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

于 2011-12-19T21:30:26.090 回答
4

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

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

于 2011-12-19T21:25:23.417 回答
4

那是不安全的,因为线程 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)or中synchronized(sumByAccount)。另一种方式,由于上述事件序列仅在两个线程同时更新同一个帐户时才会发生,因此可能是基于某种Account对象进行外部同步。没有看到你的程序逻辑的其余部分,我不能确定。

于 2011-12-19T21:26:48.013 回答
2

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

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

synchronize {
    get
    add
    put
}

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

于 2011-12-19T21:29:58.953 回答