“This is day 9 of my participation in the November Gwen Challenge. Check out the event details: The Last Gwen Challenge 2021
In the previous blog post, —–, we discussed the features of volatile in an in-depth analysis of how it is implemented:
- Volatile visibility; A read to a volatile always shows the final write to that variable;
- Volatile atomicity; Volatile is atomic for a single read/write (32-bit Long, Double), except for compound operations, such as i++;
- The UNDERLYING JVM uses “memory barriers” to implement volatile semantics
LZ takes a look at volatile in terms of the happens-before principle and the memory semantics of volatile.
Volatile and happens-before
Happend-before —– happend-before is used to determine whether data is competing and threads are safe. Happens-before ensures visibility in multithreaded environments. Let’s look at the happens-before relationship between reads and writes to volatile variables for a classic example.
public class VolatileTest { int i = 0; volatile boolean flag = false; //Thread A public void write(){ i = 2; //1 flag = true; //2 } //Thread B public void read(){ if(flag){ //3 System.out.println("---i = " + i); / / 4}}}Copy the code
According to the happens-before principle, the following relation can be obtained for the above procedure:
- According to the happens-before sequence principle: 1 happens-before 2, 3 happens-before 4;
- According to the happens-before principle for volatile: 2 happens-before 3;
- 1) Before 4) before 4) before
Operations 1 and 4 have a happens-before relationship, so 1 must be visible to 4. Some of you might say, well, operation 1 and operation 2 might be reordered, right? As you’ll see from LZ’s blog, volatile not only guarantees visibility, but also prohibits reordering. So all shared variables visible to thread A before writing to A volatile will immediately become visible to thread B after thread B reads the same volatile variable.
Memory semantics and implementation of Volataile
In THE JMM, communication between threads is implemented using shared memory. The memory semantics of volatile are:
When a volatile variable is written, the JMM immediately flusher the value of the shared variable from the thread’s local memory to main memory. When a volatile variable is read, the JMM invalidates the thread’s local memory and reads the shared variable directly from main memory
So the write memory semantics of volatile are flushed directly into main memory, and the read memory semantics are read directly from main memory. So how are the memory semantics of volatile implemented? General variables are reordered, but volatile variables are not. This affects their memory semantics, so the JMM limits reordering in order to implement the memory semantics of volatile. Its reordering rules are as follows:
Translation:
- If the first operation is a volatile read, no matter what the second operation is, it cannot be reordered. This operation ensures that operations after volatile reads are not reordered by the compiler to those before volatile reads.
- When the second operation is volatile write, reorder is not allowed regardless of the first operation. This operation ensures that operations prior to volatile writes will not be reordered by the compiler after volatile writes.
- If the first operation is volatile write and the second operation is volatile read, reorder cannot be performed.
The underlying implementation of volatile was by inserting memory barriers, but it was nearly impossible for the compiler to find an optimal arrangement to minimize the total number of inserted memory barriers, so the JMM adopted a conservative strategy. As follows:
- Insert a StoreStore barrier before each volatile write
- Insert a StoreLoad barrier after each volatile write
- Insert a LoadLoad barrier after each volatile read
- Insert a LoadStore barrier after each volatile read
The StoreStore barrier ensures that all common writes preceding volatile writes are flushed to main memory before volatile writes.
The purpose of the StoreLoad barrier is to prevent reordering of volatile writes from potentially volatile reads/writes.
The LoadLoad barrier prevents the processor from reordering volatile reads above from normal reads below.
The LoadStore barrier prevents the processor from reordering volatile reads above from normal writes below.
The VolatileTest example is shown in figure 4.
public class VolatileTest { int i = 0; volatile boolean flag = false; public void write(){ i = 2; flag = true; } public void read(){ if(flag){ System.out.println("---i = " + i); }}}Copy the code
The memory barrier legend for volatile instructions was briefly illustrated above with an example.
Volatile’s memory barrier insertion strategy is very conservative, and in practice the compiler can optimize on a case by case basis to omit unnecessary barriers as long as the volatile write-read memory semantics are not changed. As follows (from Fang Tengfei’s The Art of Java Concurrent Programming) :
public class VolatileBarrierExample { int a = 0; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite(){ int i = v1; //volatile int j = v2; //volatile read a = I + j; // v1 = I + 1; //volatile v2 = j * 2; / / write volatile}}Copy the code
An example figure without optimization is as follows:
Let’s analyze which of the above memory barrier instructions are redundant
- 1: This is definitely going to stay
- 2: Disallow reordering of all the following common writes with the above volatile read, but because of the presence of a second volatile read, that common read cannot get past the second volatile read. So we can omit it.
- 3: Ordinary reading no longer exists below, can be omitted.
- 4: retention
- 5: keep
- 6: The following is volatile, so it can be omitted
- 7: keep
- 8: keep
Therefore, 2, 3 and 6 can be omitted as follows:
The resources
- Fang Tengfei: The Art of Java Concurrent Programming