Java增量基准

Ily*_* K. 7 java atomic increment jmh

我对多线程增量的最佳性能进行了调查.我检查了基于同步,AtomicInteger和自定义实现的实现,如AtomicInteger,但使用parkNanos(1),对失败的CAS.

private int customAtomic() {
        int ret;
        for (;;) {
            ret = intValue;
            if (unsafe.compareAndSwapInt(this, offsetIntValue, ret, ++ret)) {
                break;
            }
            LockSupport.parkNanos(1);
        }
        return ret;
    }
Run Code Online (Sandbox Code Playgroud)

我基于JMH做了基准:明确执行每个方法,每个方法都消耗CPU(1,2,4,8,16次)并且只消耗CPU.每个基准测试方法在1-17线程上在Intel(R)Xeon(R)CPU E5-1680 v2 @ 3.00GHz,8 Core + 8 HT 64Gb RAM上执行.结果让我感到惊讶:

  1. CAS在1个线程中最有效.2个线程 - 与监视器类似的结果.3以上 - 比监视器差,~2次.
  2. 在大多数情况下,自定义实现比监视器好2-3倍.
  3. 但在自定义实现中,随机有时会发生执行不良.好的案例 - 50 op/microsec.,坏案例 - 0.5 op/microsec.

问题:

  1. 为什么AtomicInteger不是基于同步,它更有效率,然后是当前的impl?
  2. 为什么AtomicInteger不使用LockSupport.parkNanos(1),CAS失败?
  3. 为什么自定义实现会出现这种高峰?

CustomIncrementGraph

我尝试执行此测试几次,并且spike总是发生在不同的数字线程中.另外我在另一台机器上试过这个测试,结果是一样的.也许这是测试中的问题.在StackProfiler中自定义impl的"坏情况"中,我看到:

....[Thread state distributions]....................................................................
 50.0%         RUNNABLE
 49.9%         TIMED_WAITING

....[Thread state: RUNNABLE]........................................................................
 43.3%  86.6% sun.misc.Unsafe.park
  5.8%  11.6% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
  0.8%   1.7% org.openjdk.jmh.infra.Blackhole.consumeCPU
  0.1%   0.1% com.jad.IncrementBench$Worker.work
  0.0%   0.0% java.lang.Thread.currentThread
  0.0%   0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest._jmh_tryInit_f_benchmarkparams1_0
  0.0%   0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>

....[Thread state: TIMED_WAITING]...................................................................
 49.9% 100.0% sun.misc.Unsafe.park
Run Code Online (Sandbox Code Playgroud)

在"好的情况下":

....[Thread state distributions]....................................................................
 88.2%         TIMED_WAITING
 11.8%         RUNNABLE

....[Thread state: TIMED_WAITING]...................................................................
 88.2% 100.0% sun.misc.Unsafe.park

....[Thread state: RUNNABLE]........................................................................
  5.6%  47.9% sun.misc.Unsafe.park
  3.1%  26.3% org.openjdk.jmh.infra.Blackhole.consumeCPU
  2.4%  20.3% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
  0.6%   5.5% com.jad.IncrementBench$Worker.work
  0.0%   0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_Throughput
  0.0%   0.0% java.lang.Thread.currentThread
  0.0%   0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>
  0.0%   0.0% sun.misc.Unsafe.putObject
  0.0%   0.0% org.openjdk.jmh.runner.InfraControlL2.announceWarmdownReady
  0.0%   0.0% sun.misc.Unsafe.compareAndSwapInt
Run Code Online (Sandbox Code Playgroud)

链接到基准代码

链接到结果图.X - 线程计数,Y - thpt,op/microsec

链接到RAW日志

UPD

好的,我知道,据我所知,当我使用parkNanos时,一个线程也可以长时间保持锁定(CAS).CAS失败的线程进入休眠状态,只有一个线程正在工作并递增值.我看到,对于大并发级别,当工作量很小时 - AtomicInteger不是更好的方法.但是如果我们增加workSize,例如level = CASThrpt/threadNum,它应该可以正常工作:对于本地机器我设置了workSize = 300,我的测试结果:

Benchmark                                     (workSize)   Mode  Cnt  Score   Error   Units
IncrementBench.incrementAtomicWithWork               300  thrpt    3  4.133 ± 0.516  ops/us
IncrementBench.incrementCustomAtomicWithWork         300  thrpt    3  1.883 ± 0.234  ops/us
IncrementBench.lockIntWithWork                       300  thrpt    3  3.831 ± 0.501  ops/us
IncrementBench.onlyWithWork                          300  thrpt    3  4.339 ± 0.243  ops/us
Run Code Online (Sandbox Code Playgroud)

AtomicInteger - 获胜,锁定 - 第二名,自定义 - 第三名.但是尖峰问题仍然不明确.我忘记了java版本:Java(TM)SE运行时环境(版本1.7.0_79-b15)Java HotSpot(TM)64位服务器VM(版本24.79-b02,混合模式)

Pet*_*rey 1

在同步的情况下,它往往会与锁粘在一起,这意味着一个线程可以长时间持有锁,而不会让另一个线程公平地获取它。这对于多线程来说非常糟糕,但如果您有一个基准测试,如果只有一个线程运行相对较长的时间,该基准测试会表现得更好,那么这是非常好的。

您需要更改测试,以便在使用多个线程时比仅使用一个线程运行得更好,否则您实际上将测试哪种锁定策略的公平性策略最差。

锁定策略尝试调整锁定的执行方式,这就是它可以改变行为的原因,但它不能很好地完成工作,因为代码一开始就不应该是多线程的。