【问题标题】:Being cautious of the volatile/synchronized performance penalty小心易变/同步的性能损失
【发布时间】:2016-07-02 09:32:20
【问题描述】:

考虑在 Java 中计算成功和失败的统计类。

public class Stat {
  long successes=0, failures=0;
  public success() {successes += 1;}
  public failed() {failures += 1;}
  public logStats() { ... read the values and log them ... }
}

应该定期从另一个线程调用logStats() 方法来记录当前的统计计数器。那么这段代码是错误的,因为记录器线程可能看不到最新的值,因为没有任何东西是同步的、易失的或原子的。

因为我使用的是long,甚至volatile 都不够用。甚至这个makes the increment more expensive than without。假设以非常高的频率进行计数,而日志每分钟只运行一次,有没有办法在输入logStats() 时强制将新值分配给所有线程? only logStats() synchronized 会起作用吗?一种半边同步。我知道书上说不要这样做。我只是想了解在这种特定环境下它是否会起作用以及为什么。

另外我应该注意,只有一个线程会进行计数。

编辑请仔细阅读问题所在。我不是在问如何以不同的方式实现这一点。我在问是否以及为什么存在一些半边一致性强制,即写入线程不关心但读取线程主动强制查看最新值。我的直觉是它可能只适用于一个synchronize,但我还不能解释为什么或为什么不。

【问题讨论】:

  • 根据thisvolatile 在您的情况下是不够的。你必须使用synchronized
  • 您可以使用两个 AtomicLong 计数器。
  • @Daniel 谢谢,你说得对,仅volatile 甚至不适用于long 值。相应地改变了问题。
  • @Harald 是否有可能多个线程同时更新Stats 类中的字段,还是只有单个线程频繁更新?
  • JVM 规范是怎么说的? AtomicLong 之类的存在是有原因的。

标签: java multithreading volatile


【解决方案1】:

Java 无法提供实现“半边”并发的方法,我什至怀疑硬件也提供。

但是,您可能想质疑同步保证对您的重要性。语言确实不能保证它,但只要你在 64 位平台上运行,long 访问肯定会是“原子的”,因为你的程序不会这样做即使volatilesynchronized 都不是,它们在两个 32 位读取周期中。这不像你在同步其他任何东西,所以我怀疑你的记录器获得真正的“最新”值对你来说真的很重要,而不是那些在几百个周期前写入的值。

另外,请注意,如果此代码确实对性能至关重要,那么即使是完全非同步的访问也不是免费的。一旦您在处理器之间共享缓存线,您将在相关 CPU 之间进行缓存同步流量,虽然我没有对其进行基准测试或其他任何东西,但我怀疑将 volatile 添加到等式不会有很大的不同。与其他 CPU 通信以更改缓存行状态的延迟可能比避免一个 CPU 的指令流中的内存障碍更大。

为了避免任何这些惩罚,您可能想要做一些事情,例如有一个与其他线程共享统计信息的类,与“真实”计数器分开,您只更新一次,例如,10,000 更新到真正的柜台。这样,您还可以对共享类进行volatile 访问,而不会对编写器线程造成任何常规惩罚。

【讨论】:

  • 你的观察很中肯。如果数字稍微过时也没有坏处。这就是为什么我开始对这个问题感兴趣,如果在您提出的“那又怎样”和 100% 正确的解决方案之间存在一些廉价(和肮脏)的中间方式。
  • @Harald,阅读此答案时要记住的是,Java 应该“在任何地方运行”。这意味着 Java 语言规范 (JLS) 不会做出无法在每个合理的平台上兑现的承诺。可能有一些平台可以实现您的“半边”一致性并具有良好的性能,但也可能有一些其他理想的平台无法实现它。在这些情况下,JLS 的目标是最小的公分母。
【解决方案2】:

检查 Java 5 集合和并发类,它们提供了高于语言原语的更高级别的抽象。 在您的示例中,AtomicLong 应该可以正常工作。

【讨论】:

  • 我知道所有这些课程。我的问题不是如何实现这一点。我试图更好地理解发生前的关系以及这里是否存在。
  • 然后阅读AtomicLong 工作原理的源代码,如果您无法理解规范对此的说明,它将为您提供答案。
【解决方案3】:

你可以重构你的类:

import java.util.concurrent.atomic.AtomicLong;

public class Stats {

    private final AtomicLong successes = new AtomicLong();
    private final AtomicLong failures = new AtomicLong();

    public long success() {
        return successes.incrementAndGet();
    }

    public long failed() {
        return failures.incrementAndGet();
    }

    public void logStats() {
        final long s = successes.get();
        final long f = failures.get();
        // do stuffs with s and f
    }
}

那么您不必关心并发性,因为java.util.concurrent.atomic 中的类只有原子(然后是线程安全)方法。

【讨论】:

    【解决方案4】:

    不,仅同步方法 logStats 是不够的。在您的情况下,您应该使用原子计数器来防止竞争条件,这很可能出现在高争用情况下。或者在所有方法中使用锁/同步,这对于计数器问题来说有点矫枉过正

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-04-04
      • 2013-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-24
      • 2017-02-27
      相关资源
      最近更新 更多