Java For loop using int vs long

Viewed 96

Context

I am reading a tech document which basically tells me the difference between using int vs long in fori loop: Scenario A(int fori loop)

public class SafePoint {

    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=()->{
            for (int i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            System.out.println(Thread.currentThread().getName()+"end!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("num = " + num);
    }

}

Expected Result:

Thread-1end!
Thread-0end!
num = 2000000000

Scenario B(long fori loop)

public class SafePoint {

    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=()->{
            for (long i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            System.out.println(Thread.currentThread().getName()+"end!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("num = " + num);
    }

}

Expected Result:
num = 55406251 (or some random number less than 2000000000)
Thread-1end!
Thread-0end!

The most important concept he gives is the safepoint: We have safepoint in long fori loop due to uncounted loop but not int fori loop. Therefore in int fori loop, the sleep needs to wait for the two threads to finish and then do GC cause there is no safepoint when the threads are still in int fori loop.

Issue:

I take his idea and try to reproduce them on my local machine then it failed: Basically no matter whether I use int or long, it is always the result similar to the second one. The num get printed first.

Then after carefully thinking, it can only be due to the JVM I use: java 11 corretto.

Based on tech doc's idea, it basicaly means in Java 11 safepints exist both in counted and uncounted loop

Question:

Can anyone test on java 8 and tell me whether that is the reason?

I actually already tested: in java 8 we can observe the expected results of both A and B

Does java 11 change the way how we put Safe Points and why?

Related links:

The tech doc trying to explain on and How to do GC in fori loop: Why Thread.sleep(0) can prevent gc in rocketmq?

2 Answers

The reasons of the delayed println are explained in this answer. In short, HotSpot JIT eliminated the safepoint poll inside the counted loop, so the JVM cannot reach a safepoint while the loop is running.

To eliminate a safepoint poll from the loop, two conditions must be met:

  1. the loop is counted, i.e. has a finite number of iterations ensured by an integer counter variable, and
  2. -XX:UseCountedLoopSafepoints option is disabled.

These conditions depend on the JVM version and the command line flags.

In JDK 8, a for-loop with int variable is considered counted, while a loop with long variable is not. UseCountedLoopSafepoints is always off, unless enabled explicitly with -XX:+UseCountedLoopSafepoints.

Since JDK 10, JIT compiler got the Loop Strip Mining feature. This optimization significantly reduces the overhead of safepoint polling in a tight loop, and because of that, -XX:+UseCountedLoopSafepoints became enabled by default1. That's why you don't observe a delay in JDK 11.


1 Unless a user selects Parallel or Serial GC. Since these garbage collectors are not focused on low pauses, the JVM prefers throughput and thus does not enable UseCountedLoopSafepoints. It can be still enabled manually with -XX:+UseCountedLoopSafepoints.

ANSWER: It's because JDK8 defaults to the parallel GC, and JDK9+ to the G1 GC, and this explains all.

Proof

On an arm-chip mac, with Eclipse Temurin OpenJDK, both version 1.8 and 1.17, you get the following behaviour:

1.17 1.8
int ~50m 2000m*
long ~50m ~50m

*) This answer appears half a minute+ later, the explanation isn't that the 2 threads just get through the billion loops within the second timespan.

In other words, exactly as you describe. Given that this is temurin, it's not 'amazon coretto' specifically that's at fault.

However, if I then run the int variant on 1.17, but with:

/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home/bin/java -XX:+UseParallelGC SafePoint

It prints 2000m just like temurin-8 does (and after ~half a minute of chugging away at it).

This therefore fully explains the difference:

  • If you run this code with int and using the Parallel GC, you get 2000m (eventually; it takes a while, the threads finish before the print code happens).
  • Otherwise (you use the G1 GC, or you use long), you get ~50m; the 'print the number' code runs well before the threads finish.

If you don't explicitly choose which garbage collection impl you want, up to JDK8 you default to the parallel GC, starting with JDK9, you get the G1 collector (and perhaps on recent versions zgc or whatnot. Not the parallel collector, in any case).

It's therefore clearly not about how the JVM injects safepoints, and you can inspect this by asking the JVM to print you the machine code it is generating for you (many blogposts you can search the web for explain precisely how to do this and what to look for) - you'll find JDK17s do similar things as JDK8 in this regard.

No, instead, the Parallel GC blocks on hitting a safepoint in every thread, whereas the G1 collector does not, a much simpler explanation.

There are many reasons why the default GC impl has been changed away from the parallel collector, and this is presumably one of them.

Related