Fundamentals 8 min read

How volatile Impacts Java Threads: Experiments, JVM Modes, and Atomicity

This article demonstrates how the volatile keyword influences thread communication in Java, compares client and server JVM modes, analyzes resulting memory visibility and reordering effects, and explores atomicity limits with bytecode examples and practical recommendations.

Xiaokun's Architecture Exploration Notes
Xiaokun's Architecture Exploration Notes
Xiaokun's Architecture Exploration Notes
How volatile Impacts Java Threads: Experiments, JVM Modes, and Atomicity

1. Using volatile

The following Java program creates two threads: one writes to a shared static variable and the other reads it after a short sleep, illustrating the effect of the volatile keyword on visibility.

<code>public class VolatileUsedClass {
    private static int sharedVar = 10;
    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // modify the sharedVar, write first
                    TimeUnit.MICROSECONDS.sleep(500L);
                    sharedVar = 20;
                    System.out.printf("%s modify the shared var to %s  ...\n", "thread-1", sharedVar);
                } catch (Exception e) {
                    System.out.println(e);
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // read the value, read last
                    TimeUnit.MICROSECONDS.sleep(505L);
                    System.out.printf("%s read the shared var %s \n", "thread-2", sharedVar);
                } catch (Exception e) {
                    System.out.println(e);
                }
            }
        });
        thread2.start();
        thread1.start();
        thread1.join();
        thread2.join();
        System.out.println("finish the thread task...");
    }
}
</code>

Client mode (‑client)

Execution results without volatile (multiple runs) – see images.

Execution results with volatile (multiple runs) – see images.

Server mode (‑server)

Execution results without volatile (multiple runs) – see images.

Execution results with volatile (multiple runs) – see images.

Result analysis

The sleep calls enforce a write‑then‑read order.

Without volatile, the reading thread often sees a stale value cached locally, not the value from main memory.

With volatile, the reading thread observes the value written by the writer, demonstrating visibility and ordering guarantees.

In server mode, volatile can still lead to inconsistencies because the JIT may reorder bytecode for optimization.

2. Atomicity issues

All JDK documentation states that, except for long and double on 32‑bit CPUs, reads and writes of primitive types are atomic, and any volatile variable also provides atomic read/write.

For 64‑bit types on a 32‑bit processor, the JVM splits the operation into two 32‑bit writes, losing atomicity. The happens‑before rule ensures that reads of a volatile variable see the latest write, even for long/double.

Code example: volatile variable increment

<code>// Part of the write thread, assuming sharedVar is volatile
Thread t1 = new Thread(){
    @Override
    public void run() {
        try {
            // modify the sharedVar
            TimeUnit.MICROSECONDS.sleep(500L);
            sharedVar ++;
            System.out.printf("%s modify the shared var with atomic %s  ...\n", "thread-1", sharedVar);
        } catch (Exception e) {
            System.out.println(e);
        }
    }
};
</code>

Running the program shows that the final value may not be the expected result because the increment operation consists of a read, an addition, and a write, which is not atomic despite the volatile qualifier.

Bytecode for a simple assignment (sharedVar = 20) shows a single store operation, which is atomic: <code>L3 LINENUMBER 102 L3 BIPUSH 20 INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I POP L4 </code> Bytecode for the increment (sharedVar++) reveals an extra addition step, indicating a non‑atomic sequence: <code>L3 LINENUMBER 27 L3 INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$000 ()I ICONST_1 IADD INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I POP L4 </code> Summary of volatile usage Volatile guarantees visibility of writes to other threads and provides atomicity for single read/write actions, even for long/double. It does not make compound actions (e.g., ++, --, check‑then‑act) atomic; explicit locking is required for such cases. References: JVM Server vs Client Mode (https://javapapers.com/core-java/jvm-server-vs-client-mode/) and JDK concurrency atomicity tutorial (https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html). Thank you for reading; please share if you found this helpful.

JavaJVMConcurrencyvolatileMemory ModelAtomicity
Xiaokun's Architecture Exploration Notes
Written by

Xiaokun's Architecture Exploration Notes

10 years of backend architecture design | AI engineering infrastructure, storage architecture design, and performance optimization | Former senior developer at NetEase, Douyu, Inke, etc.

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.