谨慎对待波动/同步性能损失

时间:2016-03-16 13:14:57

标签: java multithreading volatile

考虑用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()时强制将新值分配给所有线程?是否可以仅 logStats() synchronized。一种半边同步。我知道这些书说不要这样做。我只是想了解在这个特定的环境中它是否有用以及为什么。

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

编辑请仔细阅读问题所在。我不是要问如何以不同的方式实现这一点。我问是否以及为什么存在一些半边一致性强制执行,其中写入线程并不关心,但读者线程主动强制查看最新值。我的预感是它可能只与一个synchronize一起工作,但我无法解释原因或原因。

4 个答案:

答案 0 :(得分:2)

Java没有办法实现你的"半边"并发性,我甚至怀疑硬件是否存在。

但是,您可能想要质疑同步保证对您的重要性。确实,语言并不能保证它,但只要你在64位平台上运行,long访问肯定会是" atomic"从某种意义上说,即使volatilesynchronized都没有,你的程序也不会在两个32位读周期中执行它们。它并不像你和其他任何东西同步一样,所以我怀疑你的记录器能够真正获得最新的"最新的"价值,而不是一些写回几百个循环的价值。

另外,请注意,如果此代码确实对性能至关重要,那么即使完全非同步访问也不是免费的。只要您在处理器之间共享缓存行,您就会在相关CPU之间拥有缓存同步流量,而我还没有对它进行基准测试,我怀疑是在添加volatile这个等式不会产生太大的影响。与其他CPU通信以改变缓存行状态的延迟可能比避免一个CPU指令流中的内存障碍更为严重。

为了避免任何这些处罚,你可能想做一些事情,比如有一个类与其他线程共享统计数据,与"真实"计数器,您只需更新一次,例如,对真实计数器进行10,000次更新。这样,您也可以volatile访问该共享类,而不会对编写器线程造成任何常规处罚。

答案 1 :(得分:0)

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

答案 2 :(得分:0)

您可以将您的课程重构为:

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中的类只有原子(然后是线程安全的)方法。

答案 3 :(得分:0)

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

来说有点过分