When a variable is declared volatile, the read/write of that variable is special. Volatile is lightweight synchronized. If a variable is volatile, it is less expensive than synchronized because it does not cause thread context switching and scheduling.
Java memory model
These three basic concepts are commonly encountered in concurrent programming: atomicity, visibility, and orderliness.
atomic
Atomicity means that an operation or operations are either all performed without interruption by any factor, or none at all. Like transactions in a database, they live and die together as a team. For example,
example | Atomicity or not | explain |
---|---|---|
i=1 | is | In Java, variables and assignments to primitive data types are atomic operations |
j=i | no | There are two operations: read I and assign the value of I to j |
i++ | no | There are three operations: read I, I +1, and assign the +1 result to I |
j=i+1 | no | There are three operations: read the value of I, I +1, and assign the +1 result to j |
In a single-threaded environment we can think of the entire step as atomic, but in a multi-threaded environment Java only guarantees atomic variables and assignments of primitive data types. To ensure atomicity in a multithreaded environment, use locking, synchronized (volatile does not guarantee atomicity for compound operations).
visibility
Visibility means that when multiple threads access the same variable and one thread changes the value of the variable, other threads can immediately see the changed value.
In a multithreaded environment, one thread’s operation on a shared variable is invisible to other threads. Java provides volatile to ensure visibility. When a variable is volatile, thread-local memory is invalid. When a thread changes a shared variable, it is immediately updated to main memory, and when other threads read the shared variable, it is read directly from main memory. Both Synchronize and locks ensure visibility.
order
Orderliness means that the order in which a program is executed is the order in which the code is executed.
In the Java memory model, the compiler and processor are allowed to reorder instructions for efficiency. Reordering does not affect the results of a single thread, but does affect multiple threads. Java provides volatile to ensure some order. The most famous example is the DCL (double-checked lock) in the singleton pattern.
Principle of volatile
Volatile guarantees thread visibility and provides some order, but not atomicity. Volatile is implemented using “memory barriers” underneath the JVM.
Thus, the characteristics of volatile can be summarized as follows
- Guaranteed visibility, not guaranteed atomicity (guaranteed atomicity only for read/write of individual volatile variables)
- Disallow instruction reordering
- The underlying volatile is implemented using “memory barriers.
Volatile and happens-before
Happens-before is the main basis for determining whether data is stored competitively and whether threads are safe. It ensures visibility in a multi-threaded environment.
Happens-before Rules about volatile
Volatile variable rule: Writes to a volatile field, happens-before any subsequent reads to that volatile field. When a volatile variable is written, the JMM 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 cache and reads the variable from main memory.
From the perspective of memory semantics, volatile write-read has the same memory effect as lock release-acquire. Volatile writes and lock release-acquire have the same memory semantics. Volatile reads have the same memory semantics as lock acquisition.
Sample code:
int a = 0;
volatile boolean flag = false;
//Thread A
public void writer(){
a = 1; //1
flag = true; //2
}
//Thread B
public void reader() {if(flag){ //3
int i = a; //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. At this point, 1 and 2 will not be reordered because the current flag is modified by volatile, which will prohibit 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.
Volatile memory semantics
In THE JMM, communication between threads is implemented using shared memory. 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. 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.
Volatile write-read memory semantics
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
Volatile write memory semantics
In the example above, thread A executes writer() first and thread B executes reader(). At the beginning, flag and A in the local memory of both threads are in the initial state. The following diagram shows the state of the shared variable after volatile write by thread A.
After thread A writes flag variables, the values of the two variables updated by thread A in local memory A are flushed to the main memory. In this case, the values of shared variables in local memory A and main memory are the same.
Volatile reads memory semantics
Proceed with thread B’s Reader () method, and the following is a diagram of shared variables that thread B volatile reads.
As shown in the figure, after reading the flag variable, the value contained in local memory B has been set to invalid. At this point, thread B must read the shared variable from main memory. The read operation of thread B causes the values of the shared variables in local memory B and main memory to be consistent.
If the steps of volatile write and volatile read are combined, after thread B reads A volatile variable, all the values of the shared variables visible to thread A before writing to the volatile variable become immediately visible to thread B
Volatile read-write memory semantics summary
- Writing A volatile variable on thread A essentially means that thread A expects the next thread to read the volatile variable to send A message.
- Thread B reads a volatile variable, essentially accepting a message from a previous thread that made changes to the shared variable before writing to the volatile variable.
- Thread A writes A volatile variable, and thread B reads the volatile variable. Essentially, thread A sends A message through main memory to thread B.
Volatile reordering
Volatile reorder table:
Is it possible to resort | Second operation | Second operation | Second operation |
---|---|---|---|
The first operation | Ordinary reading/writing | Volatile read | Volatile write |
Ordinary reading/writing | no | ||
Volatile read | no | no | no |
Volatile write | no | no |
- 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, reordering is not possible 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.
Volatile underlying implementation
The underlying implementation of volatile is by inserting a memory barrier.
- 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 above memory barrier insertion strategy is conservative, but it ensures that volatile memory semantics are correct for any pair of programs on any processor platform.
Volatile write memory barriers
Volatile write insert memory barrier instruction sequence diagram:
The StoreStore barrier ensures that all prior common writes are visible to any processor before volatile writes, because the StoreStore barrier guarantees that all common writes are flushed to main memory before volatile writes. The purpose of StoreLoad is to avoid reordering volatile writes from potentially volatile reads/writes. This is because the compiler is often unable to accurately determine whether a StoreLoad barrier needs to be inserted after a volatile write, such as a volarile write after which the method returns immediately. To ensure that the memory semantics of volatile are properly implemented, the JMM takes a conservative approach of inserting a StoreLoad barrier after or in front of each volatile write. Choosing to insert the StoreLoad barrier after volatile writes provides considerable performance when readers greatly outnumber writers.
Volatile reads memory barriers
Volatile read insert memory barrier instruction sequence diagram:
The LoadLoad barrier prevents processors from reordering volatile reads from ordinary writes
The above memory barrier insertion strategy for volatile writes and volatile reads is very conservative. During actual execution, the compiler can omit unnecessary barriers as long as the memory semantics of volatile write-read are not changed.
The underlying implementation of Volatile
This is what I learned from listening to the open class online.
Volatile, in some cases, is more convenient than locking. If a field is declared volatile, the Java thread memory model ensures that all threads see the same value of the variable.
See the most realistic running details of Java code by looking at Java’s assembly instructions.
Writes to volatile variables are preceded by a lock quality prefix. The lock prefix causes the CPU’s Cache to write to memory, and the write also causes other cpus to invalidate their Cache. This null operation makes the previous changes to the volatile variable immediately visible to other cpus. So its functions can be considered as following three
- Lock the main memory
- Any read must be performed after the write is complete
- Invalidate the stack cache of this value for other threads
The JMM and CPU analyze the principle of volatile
Continue analyzing this string of code
int a = 0;
volatile boolean flag = false;
//Thread A
public void writer(){
a = 1; //1
flag = true; //2
}
//Thread B
public void reader() {if(flag){ //3
int i = a; //4
}
}
Copy the code
The JMM and CPU analysis figure is shown below
- Read Old value, obtained through the bus
- Load old value, load old value into the working memory
- Use The old value. The CPU uses the old value in the working memory
- The CPU assigns the new value to the working memory
- Store new value, working memory will be the new value store, and it will send the Lock prefix instruction to the processor
- Write new value, write new value to main memory, write post unlock. (Note: The new and old values are relative here)
On volatile writes, the JVM sends the processor an instruction with a Lock prefix that causes the processor cache to be written to memory. The Lock signal ensures that the processing can monopolize any shared memory while the signal is spoken. But the Lock signal generally does not Lock the bus, but locks the cache, because the cost of locking the bus is higher than the cost of the bus. Then, the cache written back to memory by one processor invalidates the caches of other processors, according to the MESI control protocol to maintain consistency between the internal cache and the caches of other processors. That is, according to the code above, thread B’s working memory cache is flushed, and the memory is read again. This ensures visibility of volatile.
conclusion
Volatile ensures order and visibility.
- Volatile uses “memory barriers” to prevent instructions from being reordered, thereby ensuring that code execution is ordered.
- Valatile writes the processor’s cache back to memory using the Lock# prefix, ensuring visibility of variables according to the MESI control protocol.
Without changing the memory semantics of volatile write-read, the compiler can omit unnecessary barriers on a case-by-case basis.
The Art of Concurrent Programming in Java
My humble opinion, thank you for reading. Welcome to the discussion,Personal blog
JAVA concurrency (1) Concurrency programming challenges
JAVA concurrency (2) Underlying implementation principles of synchronized and volatile
JAVA concurrency (3) lock status
JAVA concurrency (4) atomic operation implementation principle
JAVA concurrency (5) happens-before
JAVA concurrency (6) reordering
JAVA concurrency (7) Analyze volatile against the JMM
JAVA concurrency (8) Final domain
JAVA concurrency (9) In-depth analysis of DCL
JAVA Concurrency (10) Concurrency programming basics