Java for循环中是否有针对线程安全性的优化?

Kid*_*nbo 33 java multithreading thread-safety

我有一段代码,可以在两个线程中更改一个计数器。它不是线程安全的,因为我没有在代码中放置任何原子变量或锁。如果代码仅运行一次,它会给出正确的结果,但是我想运行几次,因此将代码放入for循环中。问题是只有前两个循环会生成我期望的结果。对于其余的循环,结果始终为0,这似乎是线程安全的。Java虚拟机中是否有任何内部运算符导致这种情况?

我尝试过更改循环数,而第一个或第二个总是我期望的,但是无论有多少个循环,其他的都是0。

计数器:

private static class Counter {
    private int count;

    public void increase() {
        count++;
    }

    public void decrease() {
        count--;
    }

    public int getCount() {
        return count;
    }
}
Run Code Online (Sandbox Code Playgroud)

人:

// This is just a thread to increase and decrease the counter for many times.
private static class Person extends Thread {
    private Counter c;

    public Person(Counter c) {
        this.c = c;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            c.increase();
            c.decrease();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

主要方法:

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        Counter c = new Counter();
        Person p1 = new Person(c);
        Person p2 = new Person(c);
        p1.start();
        p2.start();
        p1.join();
        p2.join();
        System.out.println("run "+i+": "+c.getCount());        
   }
}
Run Code Online (Sandbox Code Playgroud)

输出:

run 0: 243
run 1: 12
run 2: 0
run 3: 0
run 4: 0
run 5: 0
run 6: 0
run 7: 0
run 8: 0
run 9: 0
Run Code Online (Sandbox Code Playgroud)

我不知道为什么其余结果总是为0。但是我想这与JVM的优化有关。完成某些循环后,JVM优化代码是否正确,并忽略其余循环并始终给出0作为答案,对吗?

Mar*_*o13 26

这发生了令人惊讶的转折。

一个人(相对确定)可以说的第一件事是影响是由JIT引起的。我将代码片段合并到此MCVE中:

public class CounterJitTest
{
    private static class Counter
    {
        private int count;

        public void increase()
        {
            count++;
        }

        public void decrease()
        {
            count--;
        }

        public int getCount()
        {
            return count;
        }
    }

    private static class Person extends Thread
    {
        private Counter c;

        public Person(Counter c)
        {
            this.c = c;
        }

        @Override
        public void run()
        {
            for (int i = 0; i < 1000000; i++)
            {
                c.increase();
                c.decrease();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException
    {
        for (int i = 0; i < 10; i++)
        {
            Counter c = new Counter();
            Person p1 = new Person(c);
            Person p2 = new Person(c);
            p1.start();
            p2.start();
            p1.join();
            p2.join();
            System.out.println("run " + i + ": " + c.getCount());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

运行它

java CounterJitTest
Run Code Online (Sandbox Code Playgroud)

导致问题中提到的输出:

run 0: 6703
run 1: 178
run 2: 1716
run 3: 0
run 4: 0
run 5: 0
run 6: 0
run 7: 0
run 8: 0
run 9: 0
Run Code Online (Sandbox Code Playgroud)

使用-Xint(解释模式)关闭JIT ,即以

java -Xint CounterJitTest
Run Code Online (Sandbox Code Playgroud)

导致以下结果:

run 0: 38735
run 1: 53174
run 2: 86770
run 3: 27244
run 4: 61885
run 5: 1746
run 6: 32458
run 7: 52864
run 8: 75978
run 9: 22824
Run Code Online (Sandbox Code Playgroud)

为了更深入地了解JIT的实际功能,我从HotSpot反汇编程序VM中开始了整个工作,以了解生成的程序集。但是,执行时间是如此之快,以至于我以为:好吧,我只是在for-loop中增加计数器:

for (int i = 0; i < 1000000; i++)
Run Code Online (Sandbox Code Playgroud)

但是即使增加它也会100000000导致程序立即完成。那已经引起了怀疑。产生反汇编后

java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly -XX:+PrintInlining CounterJitTest
Run Code Online (Sandbox Code Playgroud)

我查看了increasedecrease方法的编译版本,但没有发现明显的地方。但是,此run方法似乎是罪魁祸首。最初,run方法的汇编包含预期的代码(仅在此处发布最相关的部分):

Decoding compiled method 0x0000000002b32fd0:
Code:
[Entry Point]
[Constants]
  # {method} {0x00000000246d0f00} &apos;run&apos; &apos;()V&apos; in &apos;CounterJitTest$Person&apos;
  ...
[Verified Entry Point]
  ...
  0x0000000002b33198: je     0x0000000002b33338  ;*iconst_0
            ; - CounterJitTest$Person::run@0 (line 35)

  0x0000000002b3319e: mov    $0x0,%esi
  0x0000000002b331a3: jmpq   0x0000000002b332bc  ;*iload_1
            ; - CounterJitTest$Person::run@2 (line 35)

  0x0000000002b331a8: mov    0x178(%rdx),%edi   ; implicit exception: dispatches to 0x0000000002b3334f
  0x0000000002b331ae: shl    $0x3,%rdi          ;*getfield c
            ; - CounterJitTest$Person::run@9 (line 37)

  0x0000000002b331b2: cmp    (%rdi),%rax        ;*invokevirtual increase
            ; - CounterJitTest$Person::run@12 (line 37)
            ; implicit exception: dispatches to 0x0000000002b33354
  ...
  0x0000000002b33207: je     0x0000000002b33359
  0x0000000002b3320d: mov    0xc(%rdi),%ebx     ;*getfield count
            ; - CounterJitTest$Counter::increase@2 (line 9)
            ; - CounterJitTest$Person::run@12 (line 37)

  0x0000000002b33210: inc    %ebx
  0x0000000002b33212: mov    %ebx,0xc(%rdi)     ;*putfield count
            ; - CounterJitTest$Counter::increase@7 (line 9)
            ; - CounterJitTest$Person::run@12 (line 37)
  ...
  0x0000000002b3326f: mov    %ebx,0xc(%rdi)     ;*putfield count
            ; - CounterJitTest$Counter::decrease@7 (line 14)
            ; - CounterJitTest$Person::run@19 (line 38)

  ...
Run Code Online (Sandbox Code Playgroud)

诚然,我并没有很“理解”这一点,但是可以看到它确实做了getfield c(和部分内联吗?)increasedecrease方法的调用。

但是,该方法的最终编译版本run是这样的:

Decoding compiled method 0x0000000002b34590:
Code:
[Entry Point]
[Constants]
  # {method} {0x00000000246d0f00} &apos;run&apos; &apos;()V&apos; in &apos;CounterJitTest$Person&apos;
  #           [sp+0x20]  (sp of caller)
  0x0000000002b346c0: mov    0x8(%rdx),%r10d
  0x0000000002b346c4: 
<writer thread='2060'/>
[Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_131\lib\rt.jar]
<writer thread='5944'/>
shl    $0x3,%r10
  0x0000000002b346c8: cmp    %r10,%rax
  0x0000000002b346cb: jne    0x0000000002a65f60  ;   {runtime_call}
  0x0000000002b346d1: data32 xchg %ax,%ax
  0x0000000002b346d4: nopw   0x0(%rax,%rax,1)
  0x0000000002b346da: nopw   0x0(%rax,%rax,1)
[Verified Entry Point]
  0x0000000002b346e0: mov    %eax,-0x6000(%rsp)
  0x0000000002b346e7: push   %rbp
  0x0000000002b346e8: sub    $0x10,%rsp         ;*synchronization entry
            ; - CounterJitTest$Person::run@-1 (line 35)

  0x0000000002b346ec: cmp    0x178(%rdx),%r12d
  0x0000000002b346f3: je     0x0000000002b34701
  0x0000000002b346f5: add    $0x10,%rsp
  0x0000000002b346f9: pop    %rbp
  0x0000000002b346fa: test   %eax,-0x1a24700(%rip)        # 0x0000000001110000
            ;   {poll_return}
  0x0000000002b34700: retq   
  0x0000000002b34701: mov    %rdx,%rbp
  0x0000000002b34704: mov    $0xffffff86,%edx
  0x0000000002b34709: xchg   %ax,%ax
  0x0000000002b3470b: callq  0x0000000002a657a0  ; OopMap{rbp=Oop off=80}
            ;*aload_0
            ; - CounterJitTest$Person::run@8 (line 37)
            ;   {runtime_call}
  0x0000000002b34710: int3                      ;*aload_0
            ; - CounterJitTest$Person::run@8 (line 37)

  0x0000000002b34711: hlt    
  0x0000000002b34712: hlt    
  0x0000000002b34713: hlt    
  0x0000000002b34714: hlt    
  0x0000000002b34715: hlt    
  0x0000000002b34716: hlt    
  0x0000000002b34717: hlt    
  0x0000000002b34718: hlt    
  0x0000000002b34719: hlt    
  0x0000000002b3471a: hlt    
  0x0000000002b3471b: hlt    
  0x0000000002b3471c: hlt    
  0x0000000002b3471d: hlt    
  0x0000000002b3471e: hlt    
  0x0000000002b3471f: hlt    
[Exception Handler]
[Stub Code]
  0x0000000002b34720: jmpq   0x0000000002a8c9e0  ;   {no_reloc}
[Deopt Handler Code]
  0x0000000002b34725: callq  0x0000000002b3472a
  0x0000000002b3472a: subq   $0x5,(%rsp)
  0x0000000002b3472f: jmpq   0x0000000002a67200  ;   {runtime_call}
  0x0000000002b34734: hlt    
  0x0000000002b34735: hlt    
  0x0000000002b34736: hlt    
  0x0000000002b34737: hlt    
Run Code Online (Sandbox Code Playgroud)

这是方法的完整组装!它确实...很好,基本上什么也没有。

为了证实我的怀疑,我明确地禁止了的内联increase通过启动方法,

java -XX:CompileCommand=dontinline,CounterJitTest$Counter.increase CounterJitTest
Run Code Online (Sandbox Code Playgroud)

输出再次是预期的:

run 0: 3497
run 1: -71826
run 2: -22080
run 3: -20893
run 4: -17
run 5: -87781
run 6: -11
run 7: -380
run 8: -43354
run 9: -29719
Run Code Online (Sandbox Code Playgroud)

所以我的结论是:

JIT内联increasedecrease方法。它们仅递增和递减相同的值。内联之后,JIT很聪明,可以弄清楚对

c.increase();
c.decrease();
Run Code Online (Sandbox Code Playgroud)

本质上是没有操作的,因此恰好做到了:什么也没有。


Sir*_*Lot 15

我认为JVM正在像您说的那样在这里进行优化。

我在您的问题中添加了一些带有时间的输出,这些输出清楚地表明优化在那里发生。

public static void main(String[] args) throws InterruptedException {

    for (int i = 0; i < 10; i++) {
        final long startTime = System.currentTimeMillis();
        Counter c = new Counter();
        Person p1 = new Person(c);
        Person p2 = new Person(c);
        p1.start();
        p2.start();
        p1.join();
        p2.join();
        final long endTime = System.currentTimeMillis();
        System.out.println(String.format("run %s: %s (%s ms)", i, c.getCount(), endTime - startTime));        
   }
}
Run Code Online (Sandbox Code Playgroud)

结果:

run 0: 1107 (8 ms)
run 1: 1 (1 ms)
run 2: 0 (2 ms)
run 3: 0 (0 ms)
run 4: 0 (0 ms)
run 5: 0 (0 ms)
run 6: 0 (1 ms)
run 7: 0 (0 ms)
run 8: 0 (0 ms)
run 9: 0 (0 ms)
Run Code Online (Sandbox Code Playgroud)

程序的第一次迭代需要很多时间,而在以后的执行中几乎没有时间使用。

似乎有理由怀疑这种行为的最佳化。

使用volatile int count

run 0: 8680 (15 ms)
run 1: 6943 (12 ms)
run 2: 446 (7 ms)
run 3: -398 (7 ms)
run 4: 431 (8 ms)
run 5: -5489 (6 ms)
run 6: 237 (7 ms)
run 7: 122 (7 ms)
run 8: -87 (7 ms)
run 9: 112 (7 ms)
Run Code Online (Sandbox Code Playgroud)

  • 必须特别强调的是,JVM并不会*如题名所示那样“优化”线程安全性,而是通过消除冗余操作来优化性能。因此,无论是否进行了优化,任何现实生活中的示例所做的事情都可能完全中断,无论优化与否,优化后有时甚至会更糟。 (7认同)
  • @ jpmc26“ volatile强制所有访问都进入RAM”,因为这是一个经常听到的错误:不,它不是,以这种方式考虑它是非常危险的。JMM不处理诸如RAM之类的事情,而只是定义强制执行什么保证。这听起来像挑剔,直到人们意识到例如x86缓存一致性协议不需要立即将更改写回主存储器。 (2认同)

Dav*_*INO 6

您无法确保递增和递减变量的多线程代码始终将结果赋予0。

确保可以:

  • 同步访问Counter对象
  • Counter对象内部使用AtomicInteger

填写完整的代码count++count--不是线程安全的。在内部,它等效于以下内容:

load count     - load count from ram to the registry
increment count - increment by 1
store count    - save from the registry to ram
Run Code Online (Sandbox Code Playgroud)

但是,如果由两个线程调用,则此代码可能具有此行为

    first                             second                           ram
    ----------                        --------                         ------
                                                                       count = 0
    load count
                                      load count
    (here count in registry == 0)     (here count in the second registry == 0)

    increment count       
                                      increment count

    (here count in registry == 1)     (here count in the second registry == 1)

    store count           
                                      store count
                                                                        count == 1
Run Code Online (Sandbox Code Playgroud)

知道您不能对这个未同步的代码的真实行为承担任何责任

它取决于许多因素,例如:

  • 处理器数量
  • 递增和递减代码的执行速度
  • 处理器的种类(I7机器和Atom处理器的行为可能不同)
  • JVM实现(对于Open JDK或Oracle JVM,您可以具有不同的行为)
  • CPU负载
  • 是否执行GC流程

您知道此代码是线程不安全的。您无法尝试预测该代码在其他PC上或使用不同配置或在具有相同配置的同一台机器上可重现的任何行为,因为您无法控制JVM外部发生的情况(CPU负载其他应用程序)。


附加说明:微基准测试的副作用与某些资源尚未加载有关。在你的代码中的竞争条件可以在第一迭代,因为类更频繁的CounterPerson尚未装载(注意,同样的执行时间对于第一次迭代是更长的时间比其他人)。

  • 这不能回答问题。OP知道该程序不是线程安全的,并且正试图证明这一点,但是由于某些原因,竞赛条件似乎在第三个循环中神奇地消失了。这就是OP所要求的。 (11认同)
  • 老实说:当前接受的答案并不比这个答案“更好”,因为它读起来有点像挥舞着手“是的,那是某种优化”。它并没有真正深入到什么样的优化以及它如何解释观察到的行为。 (3认同)
  • @SirFartALot您不能说它发生得很少。您只能说,在您创建的测试中,这种情况似乎很少见。请查看更新后的答案,以了解为什么您不能肯定地说什么。 (3认同)
  • 如您所说,@ RealSkeptic很神奇,因为它不受您的控制。试图对此进行研究超出了可预见的范围。可以肯定的是,该代码并不安全,并且您无法说出是否以及何时发生竞争状况。唯一有意义的事情是删除不安全的代码。 (3认同)
  • @RealSkeptic说不能以线程安全的方式运行的代码是不可预测的,这不是对试图预测非线程安全的代码结果的问题的答案吗?听起来像是对我的答案。 (2认同)