Detailed explanation and code example of volatile keyword in Java
1、 Basic concepts
Let's add the concepts: visibility, atomicity and ordering in the JAVA memory model.
Visibility:
Visibility is a complex property because errors in visibility always go against our intuition. Usually, we can't ensure that the read thread can see the values written by other threads in time, sometimes even impossible. In order to ensure the visibility of memory writes between multiple threads, a synchronization mechanism must be used.
Visibility refers to the visibility between threads. The modified state of one thread is visible to another thread. This is the result of a thread modification. Another thread will see it right away. For example, variables modified with volatile will have visibility. Volatile modified variables do not allow internal thread caching and reordering, that is, directly modify memory. So it is visible to other threads. However, we should pay attention to a problem here. Volatile can only make the modified content visible, but it can not guarantee its atomicity. For example, volatile inta = 0; Then there is an operation a + +; This variable a has visibility, but a + + is still a non atomic operation, that is, this operation also has thread safety problems.
In Java, volatile, synchronized, and final implement visibility.
Atomicity:
Atom is the smallest unit in the world and is indivisible. For example, a = 0; (a non long and double types) this operation is indivisible, so we say this operation is an atomic operation. Another example: a + +; this operation is actually a = a + 1; it is indivisible, so it is not an atomic operation. Non atomic operations will have thread safety problems, so we need to use synchronization technology (synchronized) to make it an atomic operation. If an operation is an atomic operation, we call it atomic. Java provides some atomic classes under the concurrent package. We can learn about the usage of these atomic classes by reading the API, such as atomicinteger, atomiclong, atomicreference, etc.
In Java, synchronized and operation in lock and unlock ensure atomicity.
Order:
The Java language provides volatile and synchronized keywords to ensure the order of operations between threads. Volatile is because it contains the semantics of "prohibiting instruction reordering". Synchronized is obtained from the rule that "only one thread is allowed to lock a variable at the same time", This rule determines that two synchronization blocks holding the same object lock can only be executed serially.
The following is excerpted from the Java concurrency in practice:
The following code will have problems in a multithreaded environment.
Novisibility may continue to loop because the read thread may never see the value of ready. Even novisibility may output 0, because the reading thread may see the value written to ready, but not the value written to number. This phenomenon is called "reordering". As long as reordering cannot be detected in a thread (even if the reordering in this thread can be clearly seen in other threads), it is impossible to ensure that the operations in the thread will be performed in the order specified in the program. When the main thread first writes number and then writes ready without synchronization, the order seen by the read thread may be completely opposite to the order written.
Without synchronization, the compiler, processor and runtime may make some unexpected adjustments to the execution order of operations. In multithreaded programs that lack sufficient synchronization, if you want to judge the execution of memory operations, you can't get the correct conclusion.
This looks like a failed design, but it enables the JVM to take full advantage of the powerful performance of modern multi-core processors. For example, in the absence of synchronization, the JAVA memory model allows the compiler to reorder operations and cache values in registers. In addition, it allows the CPU to reorder operations and cache values in processor specific caches.
2、 Volatile principle
The Java language provides a slightly weaker synchronization mechanism, the volatile variable, to ensure that the update operation of the variable is notified to other threads. When a variable is declared as volatile, both the compiler and the runtime will notice that the variable is shared, so the operations on the variable will not be reordered together with other memory operations. Volatile variables are not cached in registers or invisible to other processors, so the latest written value is always returned when reading variables of volatile type.
When accessing the volatile variable, the locking operation will not be performed, so the execution thread will not be blocked. Therefore, the volatile variable is a lighter synchronization mechanism than the synchronized keyword.
When reading and writing nonvolatile variables, each thread first copies the variables from memory to the CPU cache. If the computer has multiple CPUs, each thread may be processed on a different CPU, which means that each thread can be copied to a different CPU cache.
The declared variable is volatile, and the JVM ensures that the variable is read from memory every time it is read, skipping the CPU cache step.
When a variable is defined as volatile, it has two characteristics:
1. Ensure the visibility of this variable to all threads. The "visibility" here, as described at the beginning of this article, when a thread modifies the value of this variable, volatile ensures that the new value can be synchronized to the main memory immediately and refreshed from the main memory immediately before each use. However, ordinary variables cannot do this. The value of ordinary variables needs to be transferred between processes through main memory (see Java Memory Model for details).
2. Prohibit instruction reordering optimization. For a variable decorated with volatile, an additional "load ADDL $0x0, (% ESP)" operation is performed after assignment, This operation is equivalent to a memory barrier (the subsequent instructions cannot be reordered to the position before the memory barrier during instruction reordering). When only one CPU accesses the memory, the memory barrier is not required; (what is instruction reordering: it means that the CPU allows multiple instructions to be sent separately to each corresponding circuit unit for processing not in the order specified by the program).
Volatile performance:
Volatile's read performance consumption is almost the same as that of ordinary variables, but the write operation is slightly slower because it needs to insert many memory barrier instructions into the local code to ensure that the processor does not execute out of order.
Volatile keyword code example
Two layer semantics of volatile keyword
Once a shared variable (class member variable and class static member variable) is modified by volatile, it has two layers of semantics:
1) It ensures the visibility when different threads operate on this variable, that is, when a thread modifies the value of a variable, the new value is immediately visible to other threads.
2) Instruction reordering is prohibited.
Let's look at a piece of code first. If thread 1 executes first and thread 2 executes later:
This code is a typical piece of code. Many people may use this marking method when interrupting threads. But in fact, will this code run completely correctly? That is, will the thread be interrupted? Not necessarily. Maybe most of the time, this code can interrupt the thread, but it may also lead to failure to interrupt the thread (although this possibility is very small, it will cause an endless loop once this happens).
Let's explain why this code may cause the thread to be unable to interrupt. As explained earlier, each thread has its own working memory during operation. When thread 1 runs, it will copy the value of the stop variable and put it in its own working memory.
Then, after thread 2 changes the value of the stop variable, but it has not yet written to the main memory, thread 2 turns to do other things. Thread 1 will continue to cycle because it does not know the change of thread 2 to the stop variable.
However, it becomes different after being decorated with volatile:
First, using volatile keyword will force the modified value to be written to main memory immediately;
Second: if the volatile keyword is used, when thread 2 modifies, the cache line of the cache variable stop in the working memory of thread 1 will be invalid (if it is reflected in the hardware layer, the corresponding cache line in the L1 or L2 cache of the CPU will be invalid);
Third: since the cache line of the cache variable stop in the working memory of thread 1 is invalid, thread 1 will go to the main memory to read the value of the variable stop again.
When thread 2 modifies the stop value (of course, there are two operations here. Modifying the value in the working memory of thread 2 and then writing the modified value into the memory) will invalidate the cache line of the cache variable stop in the working memory of thread 1. When thread 1 reads, it finds that its cache line is invalid. It will wait for the main memory address corresponding to the cache line to be updated, and then go to the corresponding main memory to read the latest value.
Then thread 1 reads the latest correct value.
2. Does volatile guarantee atomicity?
From the above, we know that volatile keyword ensures the visibility of operation, but can volatile ensure that the operation on variables is atomic?
Here is an example:
What is the output of this program? Maybe some friends think it's 10000. But in fact, running it will find that the results of each run are inconsistent, which is a number less than 10000.
Some friends may have questions. No, the above is the self increment operation of the variable Inc. since volatile ensures the visibility, the modified value can be seen in other threads after the self increment of Inc in each thread. Therefore, 10 threads have performed 1000 operations respectively, and the final value of Inc should be 1000 * 10 = 10000.
There is a misunderstanding. The volatile keyword can ensure that there is no mistake in visibility, but the error of the above program is that it can not ensure atomicity. Visibility can only ensure that the latest value is read every time, but volatile can't guarantee the atomicity of the operation on variables.
As mentioned earlier, the auto increment operation is not atomic. It includes reading the original value of the variable, adding 1, and writing to the working memory. In other words, the three sub operations of the auto increment operation may be executed separately, which may lead to the following situations:
If the value of the variable Inc at a certain time is 10,
Thread 1 performs the auto increment operation on the variable. Thread 1 reads the original value of the variable Inc, and then thread 1 is blocked;
Then, thread 2 performs the auto increment operation on the variable, and thread 2 also reads the original value of the variable Inc. since thread 1 only reads the variable Inc without modifying the variable, the cache line of the cache variable Inc in the working memory of thread 2 will not be invalid. Therefore, thread 2 will directly read the value of Inc in the main memory. When the value of Inc is found, 10, Then add 1, write 11 to working memory and finally to main memory.
Thread 1 then performs the add 1 operation. Since the value of Inc has been read, note that at this time, the value of Inc in the working memory of thread 1 is still 10, so after thread 1 adds 1 to Inc, the value of Inc is 11, then writes 11 to the working memory and finally to the main memory.
Then, after the two threads have performed a self increment operation respectively, Inc is only increased by 1.
At this point, some friends may have questions. No, isn't it guaranteed that a variable will invalidate the cache line when modifying the volatile variable? Then other threads will read the new value. Yes, that's right. This is the volatile variable rule in the happens before rule above. However, it should be noted that thread 1 does not modify the Inc value if it is blocked after reading the variable. Then, although volatile can ensure that thread 2 reads the value of variable Inc from memory, thread 1 does not modify it, so thread 2 will not see the modified value at all.
The root is here. Auto increment operation is not atomic, and volatile cannot guarantee that any operation on variables is atomic.
Changing the above code to any of the following can achieve the effect:
Synchronized:
Using lock:
Use atomicinteger:
summary
The above is all about the detailed explanation of the volatile keyword and code examples in Java. I hope it will be helpful to you. Interested friends can continue to refer to this website:
On the synthetic keyword in Java programming