Art is long, life is long
Preface:
The Volatile keyword is a lightweight synchronization mechanism provided by Java. The Java language has two built-in synchronization mechanisms: synchronized blocks (or methods) and volatile variables. Volatile is lighter than synchronized (often referred to as heavyweight locking) because it does not cause thread context switching and scheduling. But volatile variables are less synchronized (sometimes simpler and less expensive), and they are more error-prone to use.
I. Characteristics of volatile variables
1.1 Visibility is guaranteed, atomicity is not guaranteed
-
When a volatile variable is written, the JMM forces variables in that thread’s local memory to be flushed to main memory.
-
This write operation invalidates the cache of volatile variables in other threads.
Take a look at this code:
public class Test {
public static void main(String[] args) {
WangZai wangZai = new WangZai();
wangZai.start();
for(; ;) {if(wangZai.isFlag()){
System.out.println("hello"); }}}static class WangZai extends Thread {
private boolean flag = false;
public boolean isFlag(a){
return flag;
}
@Override
public void run(a) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = "+ flag); }}}Copy the code
If the thread changes the flag variable, the main thread will be able to access it.
But by volatile the flag variable, we print hello
private volatile boolean flag = false;
Copy the code
Each thread reads data from main memory into its own working memory as it manipulates data. If it does so and fails to write, the variable copy of the other thread that has already read is invalidated and needs to read data from main memory again.
Volatile ensures the visibility of shared variables performed by different threads. That is, when a volatile variable is modified by one thread, the other thread immediately sees the latest value when the change is written back to main memory.
1.2 forbid instruction rearrangement
Reordering needs to follow certain rules:
- A reorder operation does not reorder operations that have data dependencies.
- The purpose of reordering is to optimize performance, but no matter how reordering is done, the results of a single-threaded program cannot be changed.
What is reordering?
To improve performance, compilers and processors often reorder instructions from a given code execution order.
What are the types of reordering?
A good memory model actually loosens the rules of the processor and compiler, which means that both software and hardware technologies are fighting for the same goal: to make the execution as efficient as possible without changing the results of the program.
The JMM minimizes constraints on the bottom layer so that it can play to its strengths.
Therefore, when executing a program, the compiler and processor often reorder instructions to improve performance.
General reordering can be divided into the following three types:
- Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of single-threaded programs.
- Instruction – level parallel reordering. Modern processors use instruction-level parallelism to superimpose multiple instructions. If there is no data dependency, the processor can change the execution order of the corresponding machine instructions.
- Memory system reordering. Because the processor uses caching and read/write buffers, this makes it appear that load and store operations may be performed out of order.
So how does Volatile guarantee against reordering?
Second, memory barrier
The Java compiler inserts memory barrier instructions in place to prohibit reordering of a particular type of handler when generating a sequence of instructions.
To implement the memory semantics of volatile, the JMM restricts certain types of compiler and processor reordering. The JMM makes tables for volatile reordering rules for compilers:
Is it possible to resort | 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 |
For example, the last cell of the third line means that in program order, when the first operation is a read or write to a common variable and the second operation is a volatile write, the compiler cannot reorder the two operations.
As can be seen from the above table:
- When the second operation is a volatile write, there is no reordering, regardless of the first operation. This rule ensures that operations before volatile writes are not reordered by the compiler after volatile writes.
- When the first operation is a volatile read, no matter what the second operation is, it cannot be reordered. This rule ensures that operations after volatile reads are not reordered by the compiler to those before volatile reads.
- When the first operation is volatile write and the second is volatile read, reorder cannot be performed.
Note that volatile writes insert memory barriers at the front and at the back, whereas volatile reads insert two barriers at the back.
write
read
Starting with JDK5, the concept of happens-before was introduced to illustrate memory visibility between operations.
Third, happens-before
Definition of happens-before relationship:
- If one action happens-before the other, the execution result of the first action is visible to the second action.
- A happens-before relationship between two operations does not mean that the specific implementation of the Java platform must be executed in the order specified by the happens-before relationship. The JMM also allows reordering if the result of the reordering is the same as the result of the happens-before relationship.
This is the same as as-if-serial. Yes, the happens-before relationship is essentially the same thing as the as-if-serial semantics.
The as-if-serial semantics guarantee that the result of reordering within a single thread is the same as the result of the program code itself.
The happens-before relationship guarantees that the execution result of a properly synchronized multithreaded program will not be reordered.
To sum up, if operation A happens-before operation B, then operations done in memory by operation A are visible to operation B, whether they are on the same thread or not.
In Java, there are rules for happens-before relationships:
- Procedure order rule: For every action in a thread, happens-before any subsequent action in that thread.
- Monitor lock rule: a lock is unlocked, happens-before a lock is subsequently locked.
- Volatile variable rules: Writes to a volatile field, happens-before, and any subsequent reads to that volatile field.
- Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.
- Start rule: If thread A performs the operation ThreadB. Start () starts ThreadB, then thread A’s ThreadB. The start() operation is happens-before any operation in thread B.
- Join rule: If thread A performs the operation ThreadB. Join () and return successfully, then any action in ThreadB happens before thread A from ThreadB. The join() operation returned successfully.