Backend Development 10 min read

Why i++ Is Not Thread‑Safe and How to Demonstrate It with Java, Byteman, and Synchronization

The article explains that the Java increment operation i++ is not atomic, describes the three‑step execution that leads to race conditions in multithreaded environments, and shows how to reproduce and fix the issue using synchronized blocks, AtomicInteger, and Byteman fault‑injection scripts.

FunTester
FunTester
FunTester
Why i++ Is Not Thread‑Safe and How to Demonstrate It with Java, Byteman, and Synchronization

In earlier posts I used the paid plugin vmlens to demonstrate that the expression i++ is 100% thread‑unsafe; the demo ran two threads concurrently incrementing i++ and exposed the full race condition.

Because vmlens required a license and the author only offered a two‑week trial, I abandoned it after a brief try.

While studying the official documentation of Byteman , I realized that its fault‑injection capabilities could reproduce the same pattern as vmlens . By controlling the order of reads and writes to a shared variable, we can deliberately create a non‑atomic i++ scenario.

Why i++ Is Unsafe

The increment operation is not atomic because it consists of three steps:

Read variable value : load the current value of i from memory.

Increment : add 1 to the loaded value.

Write back : store the updated value back to memory.

In a single‑threaded program this sequence is safe, but when multiple threads execute i++ simultaneously, a race condition can occur: two threads may read the same initial value before either writes back, resulting in only one increment being applied.

Solutions

Use synchronization : the synchronized keyword guarantees that only one thread at a time can execute the increment. synchronized(this) { i++; }

Use atomic classes : Java provides AtomicInteger , which offers an atomic increment operation. AtomicInteger i = new AtomicInteger(0); i.incrementAndGet(); // equivalent to i++

These approaches prevent inconsistent updates when several threads modify the variable concurrently.

Test Code

The following simple test creates two threads; each thread sleeps one second and then calls test() , which increments the shared static variable i and prints the thread name and the new value.

package com.funtest.temp;

public class FunTester {

    static int i = 0;

    public static void test() {
        i++;
        System.out.println(Thread.currentThread().getName() + "     " + i);
    }

    public static void main(String[] args) {
        for (int j = 0; j < 2; j++) {
            new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    test();
                }
            }).start();
        }
    }
}

The logic can be summarised as:

Static variable i is shared by all threads, initialised to 0.

Method test() performs i++ and prints the result; the increment is not atomic.

main() launches two threads that repeatedly invoke test() every second.

Because i++ is not atomic, the output may show missing or duplicated values.

Byteman Rule Script

The script below injects two rules into com.funtest.temp.FunTester.test() to monitor and manipulate the increment operation.

RULE sync test
CLASS com.funtest.temp.FunTester
METHOD test
HELPER org.chaos_mesh.byteman.helper.FunHelper
AT ENTRY
IF TRUE
DO setThreadName()
ENDRULE

RULE async test
CLASS com.funtest.temp.FunTester
METHOD test
HELPER org.chaos_mesh.byteman.helper.FunHelper
AT WRITE i
IF checkThreadName()
DO System.out.println(Thread.currentThread().getName() + "     持有锁")
ENDRULE

The first rule runs unconditionally at method entry and calls setThreadName() to record the current thread name. The second rule triggers on writes to i ; if checkThreadName() returns true, it prints a message indicating that the thread holds the lock.

Practical Effect

Running the program with the Byteman rules produces console output like the following, showing that after injection each thread repeatedly reports the same value, confirming the intentional thread‑unsafe behaviour.

Thread-3      7
Thread-3      8
Thread-2      9
Thread-3      10
Thread-2      11
setThreadName  Thread-2
setThreadName  Thread-2
Thread-3      持有锁
Thread-3      12
setThreadName  Thread-3
Thread-2      持有锁
Thread-2      12
... (continues)

Before injection the output appears orderly, but after injection the values become inconsistent, demonstrating the race condition.

Conclusion

The article shows how the non‑atomic nature of i++ leads to race conditions, how to reproduce the issue with simple multithreaded Java code, and how to mitigate it using synchronization, atomic classes, or Byteman fault‑injection for testing purposes.

JavaconcurrencySynchronizationThread SafetyAtomicIntegerByteman
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.