Location>code7788 >text

The most complete analysis of the principle of volatile keyword

Popularity:943 ℃/2024-09-28 19:45:43

present (sb for a job etc)

volatile is a lightweight synchronization mechanism. volatile can be used to solve visibility and ordering problems, but does not guarantee atomicity.

The role of volatile:

  1. Visibility is guaranteed when different threads operate on a shared variable, i.e., one thread modifies the value of a variable and this new value is immediately visible to other threads.
  2. Instruction reordering is prohibited.

underlying principle

memory barrier

Volatile maintains visibility and ordering through memory barriers. The hardware-level memory barriers are divided into two main types Load Barrier,Store Barrier, i.e., Read Barrier and Write Barrier. For Java memory barriers, it is divided into four, that is, the permutation of these two barriers.

  1. Each volatile write is preceded by insertion of the StoreStore barrier; in order to prohibit reordering of previous normal and volatile writes, it also serves the purpose of brushing out the local memory data from the normal writes of the previous threads to main memory to ensure visibility;
  2. Inserts a StoreLoad barrier after each volatile write; prevents volatile writes from being reordered with volatile reads/writes that may follow;
  3. Inserts a LoadLoad barrier after each volatile read; disables reordering for all subsequent normal and volatile read operations;
  4. Insert LoadStore barrier after each volatile read. Disables all subsequent normal write operations and reordering of volatile reads;

Inserting a memory barrier is equivalent to telling the CPU and compiler that whatever comes before this command must be executed first, and whatever comes after this command must be executed later. To write to a volatile field, the Java memory model inserts a write-barrier instruction after the write operation, which flushes all previous writes to memory.

the principle of visibility

When a write operation is performed on a volatile variable, the JVM sends a Lock#-prefixed instruction to the processor

And this LOCK prefixed instruction implements two main steps:

  1. Writes data from the current processor cache line back to system memory;
  2. Invalidates cache lines in other processors that have cached this data.

The reason for this is the cache coherency protocol, where each processor checks to see if its cache is out of date through bus sniffing and the MESI protocol. When the processor discovers that the memory address corresponding to its cache line has been modified, it invalidates the current processor's cache line, and when the processor performs a modification operation on this data, it will reread the data from the system memory into the processor's cache.

Cache Coherence Protocol: When a CPU writes data, if it finds that the variable being operated on is a shared variable, i.e., a copy of the variable also exists in other CPUs, it signals the other CPUs to invalidate the cache line for that variable, so that when the other CPUs need to read the variable, it will re-read it from memory.

To summarize:

  1. When a volatile-modified variable undergoes a write operation, the JVM sends a LOCK# prefix instruction to the CPU to ensure the atomicity of the write operation through the cache coherency mechanism, and then updates the data at the corresponding main memory address.
  2. The processor will use sniffing techniques to ensure that the data in the current processor cache line, main memory and other processor cache lines are consistent on the bus. In the JVM through the LOCK prefix instruction to update the current processor's data, other processors will sniff the data inconsistency, so that the current cache line is invalidated, when you need to use the data directly to the memory to read, to ensure that the data read when the modified value.

(math.) the principle of order (physics)

The happens-before relationship of volatile

One of the happens-before rules is the volatile variable rule: a write to a volatile field happens-before any subsequent read of the volatile field.

// Assume that thread A executes the writer method and thread B executes the reader method.
class VolatileExample {
    int a = 0; volatile boolean flag = false; volatile boolean
    volatile boolean flag = false;

    public void writer() {
        a = 1; // 1 thread A modifies the shared variable
        flag = true; // 2 Thread A writes the volatile variable.
    }

    public void reader() {
        if (flag) { // 3 Thread B reads the same volatile variable
        int i = a; // 4 Thread B reads the shared variable
        ......
        }
    }
}

According to the happens-before rule, the above procedure creates 3 types of happens-before relationships.

  • According to the program order rule: 1 happens-before 2 and 3 happens-before 4.

  • According to the volatile rule: 2 happens-before 3.

  • According to the transitive rule of happens-before: 1 happens-before 4.

Because of the above rule, thread B is able to quickly sense when thread A changes the volatile variable flag to true.

volatile Disable reordering

For performance optimization, the JMM will allow the compiler and processor to reorder sequences of instructions without changing the correct semantics.The JMM provides a memory barrier to prevent such reordering.

The Java compiler inserts memory-barrier directives at appropriate places when generating instruction families to disable certain types of processor reordering.

The JMM develops a volatile reordering rule list for the compiler.

To implement volatile memory semantics when generating bytecode, the compiler inserts memory barriers into the instruction sequence to prohibit specific types of processor reordering.

It is almost impossible for a compiler to discover an optimal arrangement to minimize the total number of insertion barriers, and for this reason, the JMM adopts a conservative strategy.

  • Insert a StoreStore barrier in front of each volatile write operation.

  • Insert a StoreLoad barrier after each volatile write operation.

  • Insert a LoadLoad barrier after each volatile read operation.

  • Insert a LoadStore barrier after each volatile read operation.

A volatile write inserts separate memory barriers before and after, while a volatile read operation inserts two memory barriers after.

Why atomicity is not guaranteed

In a multithreaded environment, atomicity means that an operation or series of operations is either fully executed or not executed at all, and is not interrupted by operations in other threads.

The volatile keyword ensures that changes to a variable made by one thread are immediately visible to other threads, which is not sufficient for sequences of read-modify-write operations, which are not inherently atomic. Consider the following example:

public class Counter {
    private volatile int count = 0;

    public void increment() {
        count++; // This is actually three separate operations: reading the value of count, incrementing it by 1, and writing the new value back to count.
    }
}

In this example, the increment() method is not thread-safe, even though the count variable is declared volatile. When multiple threads call the increment() method at the same time, the following may happen:

  1. Thread A reads the current value of count as 0.
  2. Thread B also reads the current value of count as 0 (before thread A increases count).
  3. Thread A increases count to 1 and writes back.
  4. Thread B also increases count to 1 and writes back.

In this case, although the increment() method is called twice, the value of count is only incremented by 1 instead of the expected 2. This is because the count++ operation is not atomic; it involves multiple steps of reading the value of count, incrementing it by 1, and then writing back the new value. Between these steps, operations in other threads may interfere.

To ensure atomicity, you can use the synchronized keyword or an atomic class in a package (e.g., AtomicInteger), which are mechanisms that can guarantee atomicity for such operations:

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        (); // This operation is atomic
    }
}

In this modified example, the atomicity of the increment operation is guaranteed using AtomicInteger and its getAndIncrement() method. This means that even if multiple threads try to increment the counter at the same time, each call will correctly increment the value of count by 1.

About the Author.

From the first-line programmer Seven's exploration and practice, continuous learning iteration in the~

This article is included in my personal blog:https://

Public number: seven97, welcome to follow~