The volatile variable in the Java language can be regarded as a "lighter synchronized"; compared with the synchronized block, the volatile variable requires less encoding and runtime overhead, but the functionality it can achieve is only part of synchronized.
Lock
Locks provide two main features: mutual exclusion and visibility.
Volatile variables
The volatile variable has the visibility characteristics of synchronized, but does not have the atomic characteristics. This means that the thread can automatically discover the latest value of the volatile variable.
Volatile variables can be used to provide thread safety, but can only be applied to a very limited set of use cases: there is no constraint between multiple variables or between the current value of a variable and the modified value. Therefore, using volatile alone is not enough to implement counters, mutexes, or any class with invariants associated with multiple variables (e.g. "start <=end").
For simplicity or scalability, you may tend to use volatile variables instead of locks. Some idioms are easier to encode and read when using volatile variables instead of locks. In addition, the volatile variable does not cause thread blockage like a lock, and therefore rarely causes scalability problems. In some cases, the volatile variable can also provide performance advantages over locks if the read operation is much larger than the write operation.
Conditions for correct use of volatile variables
You can only use the volatile variable instead of locks in limited cases. To make the volatile variable ideal thread safety, the following two conditions must be met at the same time:
Write operations to variables do not depend on the current value.
This variable is not included in the invariant with other variables.
In fact, these conditions indicate that these valid values that can be written to the volatile variable are independent of the state of any program, including the current state of the variable.
The first condition limits prevent the volatile variable from being used as a thread-safe counter. Although an incremental operation (x++) looks like a separate operation, it is actually a combined operation composed of a sequence of read-modify-write operations that must be performed atomically, and volatile cannot provide the necessary atomic properties. Implementing the correct operation requires keeping the value of x constant during the operation, which is not possible with the volatile variable. (However, if the value is adjusted to be written only from a single thread, the first condition can be ignored.)
Most programming situations conflict with one of these two conditions, making the volatile variable not as universally applicable to thread safety as synchronized. Listing 1 shows a non-thread-safe numerical range class. It contains an invariant - the lower bound is always less than or equal to the upper bound.
Give an example
Let’s see an example below. We implement a counter. Each time the thread starts, the counter inc method will be called to add the counter to the execution environment - jdk version: jdk1.6.0_31, memory: 3G cpu: x86 2.4G
public class Counter { public static int count = 0; public static void inc() { //The delay here is 1 millisecond, making the result obvious try { Thread.sleep(1); } catch (InterruptedException e) { } count++; } public static void main(String[] args) { //Start 1000 threads at the same time to perform i++ calculations and see the actual result for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); } }).start(); } //The value of each run here may be different, maybe 1000 System.out.println("Run result: Counter.count=" + Counter.count); } } Running result: Counter.count=995
The actual operation result may be different every time. The result of the machine is: Running result: Counter.count=995. It can be seen that in a multi-threaded environment, Count does not expect the result to be 1000.
Many people think that this is a multi-threaded concurrency problem. You only need to add volatile before the variable count to avoid this problem. Then we are modifying the code to see if the result meets our expectations.
public class Counter { public volatile static int count = 0; public static void inc() { //The delay here is 1 millisecond, making the result obvious try { Thread.sleep(1); } catch (InterruptedException e) { } count++; } public static void main(String[] args) { //Start 1000 threads at the same time, perform i++ calculations, and see the actual result for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); } }).start(); } //The value of each run here may be different, possibly 1000 System.out.println("Run result: Counter.count=" + Counter.count); } }Running result: Counter.count=992
The operation result is still not as 1000 as we expected. Let's analyze the reasons below
In the Java garbage collection article, the allocation of memory at the moment of jvm is described. One of the memory areas is the jvm virtual machine stack. Each thread has a thread stack when it runs, and the thread stack saves the variable value information during thread runs. When a thread accesses a certain object's value, it first finds the value of the variable corresponding to the heap memory through the object's reference, and then loads the specific value of the heap memory variable into the thread's local memory to create a variable copy. After that, the thread no longer has any relationship with the object's heap memory variable value, but directly modifies the value of the copy variable, and at a certain moment after the modification (before the thread exits), it automatically writes the value of the thread variable copy back to the object's heap variable. This way the value of the object in the heap will change. The following picture describes the interaction of this writing
ead and load Copy variables from main memory to current working memory
use and assign Execute code to change the shared variable value
store and write refresh main memory related content with working memory data
where use and assign can appear multiple times
However, these operations are not atomic, that is, after the read load, if the main memory count variable is modified, the value in the thread working memory will not cause corresponding changes since it has been loaded, so the calculated result will be different from expected.
For variables modified by volatile, the jvm virtual machine only ensures that the value loaded from main memory to thread working memory is the latest
For example, if thread 1 and thread 2 are performing read and load operations, and find that the value of count in the main memory is 5, then the latest value will be loaded
After the heap count is modified in thread 1, it will be written into the main memory, and the count variable in the main memory will become 6.
Since thread 2 has already performed the read and load operation, the variable value of the main memory count will also be updated to 6 after the operation.
This causes concurrency to occur after two threads are modified with the volatile keyword in time.