本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来
The volatile keyword can be said to be the most lightweight synchronization mechanism provided by the Java virtual machine, but as to why it can only guarantee visibility, not atomicity, and how it disables instruction rearrangement, there are still many students who have not thoroughly understand
Believe me, stick to reading this article, you will firmly grasp a core Java knowledge point
Let's talk about its two functions first:
- Guaranteed visibility of variables in memory to threads
- Disable instruction reordering
I know every word, it's numb when put together
These two functions are usually not easy for us Java developers to understand correctly and completely, so that many students cannot use volatile correctly
About visibility
Not much bb, code here
public class VolatileTest {
private static volatile int count = 0;
private static void increase() {
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
}).start();
}
// 所有线程累加完成后输出
while (Thread.activeCount() > 2) Thread.yield();
System.out.println(count);
}
}
The code is easy to understand. Ten threads are opened to accumulate the same shared variable count, and each thread accumulates 1w times.
We have decorated count with volatile, which has guaranteed the visibility of count to ten threads in memory. It stands to reason that the value of count should be 10w after ten threads are executed.
However, after running it many times, the results are far less than the expected value
. Which link is the problem?
You must have heard a sentence: volatile only guarantees visibility, not atomicity
This sentence is the answer, but many people still do not understand the mystery
It's a long story and I'll keep it short. In short, the count++ operation is not atomic, it is performed in three steps.
- Read the value of count from memory
- execute count + 1
- write the new value of count back
To fully understand this problem, we have to start with bytecode
The following is the bytecode compiled by the increase method.
It doesn't matter if you don't understand it, let's look at it line by line:
- GETSTATIC: read the current value of count
- ICONST_1: load constant 1 to the top of the stack
- IADD: +1 for implementation
- PUTSTATIC: Write the latest value of count
ICONST_1 and IADD are actually real ++ operations
The key point is that volatile can only ensure that the value obtained by the thread in the GETSTATIC step is the latest, but when the thread executes the following lines of instructions, other threads may modify the value of count during this period, which will eventually lead to The old value overwrites the real new value
understand me
Therefore, in concurrent programming, it is unreliable to rely only on volatile to modify shared variables. In the end, it is necessary to lock key methods to ensure thread safety.
Just like the demo above, a little modification can achieve true thread safety
The simplest is to add a synchronized to the increase method (I will not be verbose about how synchronized achieves thread safety. I have talked about the underlying implementation principle of synchronized before )
private synchronized static void increase() {
++count;
}
Run it a few times,
isn't it all right?
By now, you should have a new understanding of the following two points
- volatile guarantees the visibility of the variable in memory to the thread
- volatile only guarantees visibility, not atomicity
About instruction rearrangement
In concurrent programming, in order to improve execution efficiency, the CPU itself and the virtual machine will use instruction rearrangement (under the premise of ensuring that the results are not affected, some codes are executed out of order)
- About cpu: in order to utilize cpu, the actual execution of instructions will be optimized;
- About virtual machine: In HotSpot vm, in order to improve execution efficiency, JIT (just-in-time compilation) mode will also perform instruction optimization
Instruction rearrangement can indeed improve execution efficiency in most scenarios, but some scenarios are strongly dependent on the order of code execution. In this case, we need to disable instruction rearrangement. The
pseudocode for the following scenario is taken from "In-depth Understanding of Java Virtual Machine" :
The scenario described is a common configuration reading process in development, but we generally do not experience concurrency when processing configuration files, so we did not realize that this would be a problem.
Just imagine, if the volatile modification is not used when defining the initialized variable, the last code "initialized=true" in thread A may be executed in advance due to the optimization of instruction reordering (although Java is used as pseudo code here, all the Refers to the reordering optimization is a machine-level optimization operation, and early execution means that the assembly code corresponding to this statement is executed in advance), so that the code using configuration information in thread B may have errors, and volatile prohibits instruction rearrangement by you can avoid this from happening
Disabling instruction rearrangement only requires declaring the variable as volatile, isn't it magic?
Let's take a look at how volatile disables instruction reordering
Let's also borrow an example from "Understanding Java Virtual Machine", it is easier to understand.
This is the implementation of the singleton mode. The following is part of its bytecode. The red box mov%eax, 0x150(%esi) is the assignment to instance
It can be seen that after the assignment, the lock addl$0x0, (%esp) instruction is also executed. The key point is here. This line of instruction is equivalent to setting a memory . After the memory barrier, the cpu or virtual The machine cannot advance the instructions behind the memory barrier to the front of the memory barrier when the instructions are rearranged. Take a good look at this passage.
Finally, leave a question that can deepen everyone's understanding of volatile. Brothers, think about it:
The Java code is obviously executed from top to bottom. Why does the problem of instruction rearrangement occur?
ok i'm done