SoFunction
Updated on 2025-03-08

Relationship between volatile and happens-before and memory consistency error

volatile variable

volatile is a keyword in Java, and we can use it to modify variables or methods.

Why use volatile
The typical usage of volatile is that when multiple threads share variables, we need to avoid memory consistency due to memory buffered variables (Memory Consistency Errors) When error.

Consider the following examples of producer-consumer, at a moment we produce or consume a unit.

public class ProducerConsumer {
 private String value = "";
 private boolean hasValue = false;
 public void produce(String value) {
 while (hasValue) {
 try {
 (500);
 } catch (InterruptedException e) {
 ();
 }
 }
 ("Producing " + value + " as the next consumable");
  = value;
 hasValue = true;
 }
 public String consume() {
 while (!hasValue) {
 try {
 (500);
 } catch (InterruptedException e) {
 ();
 }
 }
 String value = ;
 hasValue = false;
 ("Consumed " + value);
 return value;
 }
}

In this example, the produce method produces a new value and saves it in the value variable, and has the hasValue flag position to true. While loop checks whether hasValue is true. If true, the data generated by the flag has not been consumed. If true, the current thread is sleepy. When hasValue is set to false, the sleep cycle will stop, that is, the data is consumed by the consumption method. If no data is available, the cosume method will hibernate. When the producer method generates a new data, the consumption ends sleep, consumes the data, and clears the hasValue flag.

Now imagine that two threads use the same object of the class - one to generate data (write thread) and the other to consume data (read thread). The example code is as follows,

public class ProducerConsumerTest {
 @Test
 public void testProduceConsume() throws InterruptedException {
 ProducerConsumer producerConsumer = new ProducerConsumer();
 List<String> values = ("1", "2", "3", "4", "5", "6", "7", "8",
 "9", "10", "11", "12", "13");
 Thread writerThread = new Thread(() -> ()
 .forEach(producerConsumer::produce));
 Thread readerThread = new Thread(() -> {
 for (int i = 0; i > (); i++) {
 ();
 }
 });
 ();
 ();
 ();
 ();
 }
}

In most cases, this example outputs the expected results, but there is also a high possibility of enteringDeadlockstate!

Why does this phenomenon occur?

First, let’s introduce some knowledge of computer architecture.

We know that computers include CPU and memory units (and other components). The memory in which program instructions and variables are located becomes the main memory; during program execution, for better performance, the CPU may store copies of variables in its internal memory (that is, CPU buffer). Since the computer now includes more than one CPU, it also includes multiple CPU buffers.

In a multithreaded environment, multiple threads may run at the same time, each on a different CPU (determined by the underlying OS), and they may copy variables from main memory into the corresponding CPU buffer. When a thread accesses these variables, it accesses these buffered variables, not the actual variables located in the main memory.

Now let's assume that the two threads in the previous example run on two different CPUs, and the hasValue variable is buffered on one of the CPUs (or two). Consider the following execution sequence:

The thread generates a data and sets hasValue to true. However, this change is only reflected in the CPU buffering, not the main memory.
The thread is ready to consume one data, but its CPU buffer hasValue is false. So even if the writer thread generates a data, the reader thread cannot consume the data.
3. Since the reader thread cannot consume the newly generated data, the writer thread cannot continue to generate new data (because hasValue is true), the writer will sleep.
4. Then a deadlock appears!

This situation changes when the hasValue value is synchronized in all buffers (based on the underlying OS).

Solution? How does volatile apply this example?

If we set hasValue to volatile, then we can guarantee that this type of deadlock will not appear.

private volatile boolean hasValue = false;
After setting a variable to volatile, the thread will read the value of the variable directly from the main memory, and the write to the variable will be immediately refreshed to the main memory. If a thread buffers the variable, each read and write operation will be synchronized with the main memory.

After this modification, consider the above step that may cause deadlock:

A new data is generated and hasValue is set to true. The update will be directly reflected in main memory (even if the thread uses cache).
The thread tries to consume a variable and checks the value of hasValue. Each read of this variable is obtained directly from the main memory, so it can obtain changes caused by the writer thread.
The thread consumes the variable and clearly hasValue flag bit. This variable is flushed to main memory (if cached, the cached variables are also flushed).
4. Since the reader thread operates on the main memory every time, the writer thread can see changes caused by the reader. It will continue to generate new data.

volatile and happens-before relationship

Accessing the volatile variable establishes happens-before relationship between statements. When a volatile variable is written, it establishes a happens-before relationship with the subsequent read operation of the variable. So what is happens-before relationship? You can refer to the author's previous blog [Java Concurrency Programming Extra Part 2) happens-before relationship]. In short, it is to ensure that the influence of one statement will be seen by another statement (https:///article/)。

Consider the following example,

// Definition: Some variables
private int first = 1;
private int second = 2;
private int third = 3;
private volatile boolean hasValue = false;
// First Snippet: A sequence of write operations being executed by Thread 1
first = 5;
second = 6;
third = 7;
hasValue = true;
// Second Snippet: A sequence of read operations being executed by Thread 2
("Flag is set to : " + hasValue);
("First: " + first); // will print 5
("Second: " + second); // will print 6
("Third: " + third); // will print 7

Let's assume that two fragments on both sides run on two threads - Thread 1 and Thread 2. When Thread 1 modifies the hasValue value, not only the hasValue value will be directly written to the main memory, but the previous three write operations will also be written to the main memory (and other previous write operations). So when thread 2 accesses these three variables, it sees the modifications made by thread 1 to these variables, even if they will be cached (these caches will be updated).

This is also why in the first example we did not set the value variable to volatile. This is because the write operations of other variables before accessing hasValue and the read operations of other variables after reading hashValue will be automatically synchronized with the main memory.

This is another interesting sequence. JVM is famous for its program optimization. Sometimes, without affecting the output, the JVM reorders the instructions to obtain better performance. As an example, it might take the code of the sequence,

first = 5;
second = 6;
third = 7;

Reorder to,

first = 5;
second = 6;
third = 7;
However, when a statement involves accessing the volatile variable, the JVM will not place the statement before a volatile write operation after the volatile write operation. That is, it will not take the following code sequence,
first = 5; // write before volatile write
second = 6; // write before volatile write
third = 7; // write before volatile write
hasValue = true;

Modify to

first = 5;
second = 6;
hasValue = true;
third = 7; // Order changed to appear after volatile write! This will never happen!
Even from a code correctness point of view, the two are the same. Note that the JVM still allows reordering the first three statements as long as they are before the volatile write.

Similarly, the JVM does not reorder the code that is after the volatile read before the volatile read. That is to say, the code,

("Flag is set to : " + hasValue); // volatile read
("First: " + first); // Read after volatile read
("Second: " + second); // Read after volatile read
("Third: " + third); // Read after volatile read

It will not be modified to


("First: " + first); // Read before volatile read! Will never happen!
("Fiag is set to : " + hasValue); // volatile read
("Second: " + second); 
("Third: " + third);

However, the JVM can reorder the last three statements as long as they are read after volatile.

Performance overhead brought by volatile

The volatile forces main memory access, which is usually slower than CPU cache access. At the same time, it also prevents some program optimizations by the JVM, further reducing performance.

Can volatile be used to ensure data consistency of multiple threads?

The answer is no. When multiple threads access the same variable, flagging the variable as volatile is not enough to ensure consistency. Consider the following UnsafeCounter class.

public class UnsafeCounter {
 private volatile int counter;
 public void inc() {
 counter++;
 }
 public void dec() {
 counter--;
 }
 public int get() {
 return counter;
 }
}

Test the code,

public class UnsafeCounter {
 private volatile int counter;
 public void inc() {
 counter++;
 }
 public void dec() {
 counter--;
 }
 public int get() {
 return counter;
 }
}

The code is easy to read. We increase the counter's value in one thread and then decrease the counter's value in another thread. Run this test and the counter result we expect is 0, but this is not guaranteed. Mostly it is 0, however, in some cases it may be any number -2, -1, 1, 2, or even [-5, 5].

Why does this happen? This is because the increase and decrease operations of counter variables are not atomic operations - they are not executed in one go. They all include multiple steps, and the two step sequences overlap. You can think of auto-increase operations like this:

1. Read counter value
2. Add 1
3. Write the value to the counter

Similarly, self-decrease operation:

1. Read counter value
2. Reduce 1
3. Write the value to the counter

Now, we consider the following execution sequence:

1. The first thread reads the counter value from memory. It is initialized to 0. The thread then increments it.
2. The second thread also reads the counter value from memory, and the value is also 0. The thread then performs a self-decreasing operation on it.
3. The first process writes the value into memory, that is, the value of counter is 1.
4. The second thread writes the value into memory, that is, the value of counter is -1.
5. The update of the first thread is lost.

How to stop this phenomenon?

1. Use Sync:

public class SynchronizedCounter {
 private int counter;
 public synchronized void inc() {
 counter++;
 }
 public synchronized void dec() {
 counter--;
 }
 public synchronized int get() {
 return counter;
 }
}

2. Or use AtomicInteger:

public class AtomicCounter {
 private AtomicInteger atomicInteger = new AtomicInteger();
 public void inc() {
 ();
 }
 public void dec() {
 ();
 }
 public int get() {
 return ();
 }

My choice was to use AtomicInteger because the synchronization method only allows one thread to access the inc/dec/get method, which brings additional performance overhead.

When using the synchronization method, we did not set the counter to the volatile variable. This is because using synchronized keywords creates happens-before relationship. Enter a synchronous method (code block), and the code in the code before the statement and the code in the method (code block) establishes happens-before relationship.A brief discussion on happens-before of Java memory modelYou can view the detailed introduction.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.