The Java language’s volatile variables can be thought of as a form of “lesser synchronized”; ** Volatile variables require less coding and have less runtime overhead than synchronized blocks, but they do only a fraction of what synchronized does.
Locks and volatile
Locks provide two main features: atomicity and visibility.
Atomicity means that only one thread at a time is allowed to hold a particular lock, and only one thread at a time can use shared data. Visibility is the need to ensure that changes made to shared data before the lock is released are visible to another thread that subsequently acquires the lock.
Volatile variables have the visibility of synchronized, but not atomic properties. When a variable is defined as volatile, it has:
**1. Ensure that the variable is visible to all threads. When a thread changes the value of the variable, volatile ensures that the new value is immediately synchronized to main memory and flushed from main memory before any other thread uses it. ** This is not the case with ordinary variables, whose values are passed between threads through main memory. 2. Disable instruction reordering optimization. For volatile variables, an additional “load ADDL $0x0, (%esp)” operation is performed after the assignment. This operation acts as a memory barrier (instructions cannot be reordered before the barrier).
Let’s take a closer look at instruction reordering through orderliness.
Instruction reordering
Orderliness: ** refers to the order in which programs are executed in order of code. ** For a simple example, look at this code:
int i = 0; boolean flag = false; i = 1; // statement 1 flag = true; / / 2Copy the code
If statement 1 comes before statement 2, does the JVM guarantee that statement 1 will come before statement 2 when it actually executes this code? ** Not necessarily, why? Instruction Reorder can occur here. Instruction reordering:
Generally speaking, in order to improve the efficiency of the program, the processor may optimize the input code. It does not ensure that the execution sequence of each statement in the program is the same as that in the code, but it will ensure that the final execution result of the program is the same as the result of the code execution sequence.
For example, in the code above, it makes no difference which statement 1 or statement 2 executes first, so it is possible that statement 2 executes first and statement 1 follows during execution. The processor will reorder the instructions, but it will guarantee that the final result of the program will be the same as the sequence of code execution, so how does it guarantee that? By data dependency:
The compiler and processor adhere to data dependencies when reordering, and do not change the order in which two operations with data dependencies are executed.
Here’s an example of code
Double PI = 3.14; //A double r = 1.0; //B double area = pi * r * r; //CCopy the code
The data dependencies for the above three operations are shown below
There is A data dependency relationship between A and C, and there is A data dependency relationship between B and C. Therefore, C cannot be reordered before A and B in the final instruction sequence. But there is no data dependency between A and B, and the compiler and processor can reorder the execution order between A and B. Here are the two execution sequences of the program:
In computers, software technology and hardware technology have a common goal: to develop as much parallelism as possible without changing the results of program execution. Both the compiler and processor comply with this goal. The data dependency mentioned here only applies to instruction sequences executed in a single processor and operations executed in a single thread. In a single-threaded program, reordering operations with control dependencies does not change the execution result. But in multithreaded programs, reordering operations that have control dependencies may change the program’s execution results. This is where memory barriers are required to ensure visibility.
The memory barrier
Memory barriers are classified into Load barriers and Store barriers. These are read barriers and write barriers. Memory barriers serve two purposes:
1. Prevent reordering of instructions on both sides of the barrier;
2. Force the dirty data in the write buffer or cache to be written back to the main memory to invalidate the corresponding data in the cache.
- For Load barriers, inserting them before instructions invalidates data in the cache and forces data to be reloaded from main memory.
- In the case of a Store Barrier, inserting a Store Barrier after an instruction allows the latest data to be written to main memory, making it visible to other threads.
Java memory barriers are usually called four LoadLoad, namely StoreStore, LoadStore, StoreLoad is actually the combination of the two, to complete a series of barriers and data synchronization function.
**LoadLoad barrier: ** For such statements Load1; LoadLoad; Load2: Ensure that the data to be read by Load1 is completed before the data to be read by Load2 and subsequent read operations are accessed. **StoreStore barrier: ** For statements like Store1; StoreStore; Store2. Before Store2 and subsequent write operations are executed, ensure that the write operations of Store1 are visible to other processors. **LoadStore barrier: ** For such statements Load1; LoadStore; Store2, ensure that the data to be read by Load1 is completed before Store2 and subsequent write operations are flushed out. **StoreLoad barrier: ** For statements like Store1; StoreLoad; Load2, ensure that writes to Store1 are visible to all processors before Load2 and all subsequent reads are executed. It is the most expensive of the four barriers. In most processor implementations, this barrier is a universal barrier that doubles as the other three memory barriers
Volatile memory barriers are very conservative, and volatile a very pessimistic and insecure state of mind:
Insert the StoreStore barrier before each volatile write and the StoreLoad barrier after each write.
Insert a LoadLoad barrier before each volatile read and a LoadStore barrier after each volatile read.
Because memory barriers prevent reordering of volatile variables from other instructions, and threads communicate with each other, volatile behaves like a lock.
Volatile performance:
Volatile has almost the same read cost as normal variables, but writes are slower because it requires inserting many memory-barrier instructions into the native code to keep the processor from executing out of order.