Understanding Thread.sleep(0) and JVM Optimizations

The behavior of Thread.sleep(0) within a tight loop in Java, particularly concerning its interaction with Just-In-Time (JIT) compilation and safepoints, has been a subject of inquiry. This article delves into why adding Thread.sleep(0) or modifying data types can alter program execution, especially when dealing with methods like AtomicInteger.getAndAdd.

Previously, an example code snippet demonstrated a scenario where two threads concurrently incremented an AtomicInteger within a loop.

import java.util.concurrent.atomic.AtomicInteger;

public class MainTest {
    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() + " execution finished!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000); // Main thread sleeps for 1 second
        System.out.println("num = " + num);
    }
}

The expectation was that the main thread, after sleeping for 1000 milliseconds, would print the value of num. However, the program would often wait for both t1 and t2 to complete before proceeding. This behavior can be influenced by the Java Development Kit (JDK) version.

One solution to achieve the expected behavior, where the main thread doesn't wait indefinitely, is to change the loop counter or the AtomicInteger type from int to long. Another approach, which became a point of discussion, involves inserting Thread.sleep(0) within the loop.

// Modified Runnable with Thread.sleep(0)
Runnable runnable = () -> {
    for (int i = 0; i < 1000000000; i++) {
        // Adding Thread.sleep(0) here
        try {
            Thread.sleep(0);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        num.getAndAdd(1);
    }
    System.out.println(Thread.currentThread().getName() + " execution finished!");
};

This seemingly insignificant addition allows the main thread to proceed after its sleep duration. The underlying reason for this behavior is related to JIT compilation and safepoints.

The Role of JIT Compilation and Safepoints

The JIT compiler optimizes "hot code" – frequently executed methods or loop bodies. In the example, num.getAndAdd(1) within the loop is identified as hot code. The JIT compiler optimizes this code to enhance performance. A crucial aspect of JVM operation is the concept of safepoints, which are points in the execution where the JVM can safely pause threads for operations like garbage collection.

Normally, native method calls, like those underlying Thread.sleep() and AtomicInteger.getAndAdd(), provide opportunities for threads to reach safepoints. However, JIT optimizations can eliminate these opportunities.

Hypothesis:

  1. AtomicInteger.getAndAdd() involves native calls and should theoretically provide safepoint opportunities.
  2. The JIT compiler, recognizing the getAndAdd call within the loop as hot code, optimizes it, potentially removing the safepoint opportunities that would otherwise exist.

Verifying the Hypothesis

To test this, the JIT compiler can be disabled using the JVM argument -Djava.compiler=NONE. Running the original code without JIT compilation should yield the expected behavior where the main thread proceeds after 1000ms.

Upon running with -Djava.compiler=NONE, the program execution aligns with the expectation, indicating that JIT optimization was indeed the cause of the threads blocking the main thread.

To further investigate the mechanics, tools like JITWatch can be used to examine the assembly code generated by the JIT compiler before and after optimization. This tool can reveal the presence or absence of safepoint poll instructions ({poll} or {poll return}).

When analyzing the compiled code for the loop body:

  • Without Thread.sleep(0) (and with JIT enabled): In heavily optimized code (e.g., C2 compilation), safepoint poll instructions might be absent, indicating that the JIT removed them.
  • With Thread.sleep(0) (or other such interventions): The presence of Thread.sleep(0) can prevent the JIT from performing overly aggressive optimizations, thereby preserving safepoint poll instructions.

Safepoint Polls and Optimization

The JVM periodically checks for a "safepoint request" flag. This check, or "poll," incurs some overhead. The JVM aims to minimize thece polls. Safepoint polls can occur at several locations:

  1. Interpreter Mode: Between any two bytecodes. Running with -Xint forces the JVM into interpreter mode, where safepoint polls are more frequent.
  2. Compiled Code (C1/C2): On the back edge of "non-counted" loops, and at method entry/exit.
    • Crucially, method inlining by the compiler can lead to the removal of safepoint polls at method exits.
    • The presence of instructions like Thread.sleep(0) can act as a barrier, preventing the JIT from inlining excessively or performing certain aggressive optimizations that would remove safepoint polls.

Illustrative Examples

Author Nitsan Weisberg's blog posts offer detailed insights and examples regarding safepoints.

Example 0: Long TTSP Hangs Application

A nested loop structure, intended to run for a long time, was observed to hang indefinitely in some environments (like Eclipse's internal compiler) but not others (like standard javac followed by JVM execution), highlighting compiler differences. Pre-warming the code by running it for a short duration before starting the main observation can influence behavior.

Example 4: Adding Safepoint Polls Stops Optimization

This example, related to a discussion from Netty's GitHub issues, tests the performance impact of int versus long loops. It illustrates how interventions, like those discussed, can affect JIT optimizations and, consequently, safepoint poll behavior. The core idea is that certain code structures or inserted statements can signal to the JIT that more caution is needed, thus preserving safepoint opportunities.

Tags: JVM JIT Safepoint Optimization AtomicInteger

Posted on Tue, 06 Oct 2026 16:29:39 +0000 by thors1982