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.
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() + " 持有锁")
ENDRULEThe 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.
FunTester
10k followers, 1k articles | completely useless
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.