In order to solve the problems of atomicity, visibility and order in concurrent programming, the Java language provides a series of keywords related to concurrent processing, such as , , , synchronized
and volatile
so final
on concurren包
. In the [previous][2] article, we also introduced synchronized
the usage and principle of . In this article, let's analyze another keyword - volatile
.
This article revolves around volatile
it, mainly introducing volatile
the usage, volatile
principles, and volatile
how to provide visibility and order guarantees.
volatile
This keyword exists not only in the Java language, but also in many languages, and its usage and semantics are also different. Especially in C language, C++ and Java, there are volatile
keywords. Both can be used to declare variables or objects. The following is a brief introduction to the keywords in the Java language volatile
.
usage of volatile
volatile
It is usually compared to "lightweight synchronized
", and it is also a more important keyword in Java concurrent programming. And synchronized
different, volatile
is a variable modifier, can only be used to modify variables. Methods and code blocks cannot be modified.
volatile
volatile
The usage of is relatively simple, you only need to use modification when declaring a variable that may be accessed by multiple threads at the same time .
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
The above code is a typical implementation of a singleton in the form of double lock verification, in which volatile
keywords are used to modify the singleton that may be accessed by multiple threads at the same time.
The principle of volatile
In [If someone asks you what the Java memory model is, send him this article][1], we have introduced that in order to improve the execution speed of the processor, a multi-level cache is added between the processor and the memory. to improve. However, due to the introduction of multi-level cache, there is a problem of cache data inconsistency.
However, for volatile
variables, when volatile
the variable is written, the JVM will send a lock prefix instruction to the processor to write the variable in the cache back to the system main memory.
But even if it is written back to the memory, if the values cached by other processors are still old, there will be problems in performing calculation operations. Therefore, under multi-processors, in order to ensure that the caches of each processor are consistent, it will be implemented.缓存一致性协议
Cache consistency protocol : Each processor checks whether the value of its cache has expired by sniffing the data propagated on the bus. When the processor finds that the memory address corresponding to its cache line has been modified, it will update the current processor's The cache line is set to an invalid state. When the processor wants to modify the data, it will be forced to re-read the data from the system memory into the processor cache.
Therefore, if a variable is volatile
modified, its value will be forced to be flushed into main memory after each data change. The caches of other processors will also load the value of this variable from the main memory into their own caches because they comply with the cache coherency protocol. This ensures that volatile
a value is visible in multiple caches in concurrent programming.
volatile and visibility
Visibility means that when multiple threads access the same variable, one thread modifies the value of the variable, and other threads can immediately see the modified value.
We analyzed it in [If someone asks you what the Java memory model is, send him this article][1]: The Java memory model stipulates that all variables are stored in the main memory, and each thread has its own The working memory of the thread stores the main memory copy of the variables used in the thread in the working memory of the thread. All operations on variables by the thread must be performed in the working memory, and the main memory cannot be directly read and written. Different threads cannot directly access variables in each other's working memory, and the transfer of variables between threads requires data synchronization between their own working memory and main memory. Therefore, it may happen that thread 1 changes the value of a variable, but thread 2 is not visible.
As mentioned above volatile
, the keywords in Java volatile
provide a function, that is, the variables modified by them can be synchronized to the main memory immediately after being modified, and the variables modified by them are reset from the main memory before each use. Main memory is flushed. Therefore, it can be used volatile
to ensure the visibility of variables during multi-threaded operations.
volatile and ordered
Sequence means that the order of program execution is executed in the order of code.
We analyzed it in [If someone asks you what the Java memory model is, send him this article][1]: In addition to the introduction of time slices, due to processor optimization and instruction rearrangement, the CPU may also Input code for out-of-order execution, e.g. load->add->save
may be optimized into load->save->add
. This is where there may be an orderly problem.
In volatile
addition to ensuring the visibility of data, it also has a powerful function, that is, it can prohibit instruction rearrangement optimization, etc.
Ordinary variables only guarantee that the correct results can be obtained in the places where the assignment results depend on during the execution of the method, but cannot guarantee that the order of variable assignment operations is consistent with the execution order in the program code.
Volatile can prohibit the rearrangement of instructions, which ensures that the program of the code will be executed in strict accordance with the order of the code. This ensures orderliness. The operation of volatile
the modified variable will be executed strictly according to the code sequence, and load->add->save
the execution sequence is: load, add, save.
volatile and atomicity
Atomicity means that an operation is uninterruptible, and it must be executed completely, or it will not be executed at all.
What is going on with our multithreading problem in [Java's concurrent programming? ][3] analyzed: thread is the basic unit of CPU scheduling. The CPU has the concept of time slices, and will perform thread scheduling according to different scheduling algorithms. When a thread starts executing after obtaining the time slice, after the time slice is exhausted, it will lose the right to use the CPU. Therefore, in a multi-threaded scenario, since time slices are rotated between threads, atomicity problems will occur.
In the previous article, synchronized
when we introduced it, we mentioned that in order to ensure atomicity, we need to pass bytecode instructions monitorenter
and monitorexit
, but volatile
there is no relationship between these two instructions.
Therefore, volatile
atomicity cannot be guaranteed.
volatile
Can be used instead in the following two scenarios synchronized
:
1. The result of the operation does not depend on the current value of the variable, or it can ensure that only a single thread will modify the value of the variable.
2. Variables do not need to participate in invariant constraints with other state variables.
In addition to the above scenarios, other methods need to be used to ensure atomicity, such as synchronized
or concurrent包
.
Let's look at an example of volatile and atomicity:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
The above code is relatively simple, that is, create 10 threads, and then perform 1000 i++
operations respectively. Under normal circumstances, the output of the program should be 10000, but the results of multiple executions are all less than 10000. This is actually volatile
the reason why atomicity cannot be satisfied.
Why does this happen? It is because although volatile can guarantee inc
visibility between multiple threads. But inc++
the atomicity cannot.
Summary and reflection
We covered volatile
keywords and synchronized
keywords. We now know that synchronized
atomicity, ordering, and visibility can be guaranteed. But volatile
only order and visibility can be guaranteed.
So, let's take a look at the singleton implemented by the double-checked lock. It has already been used synchronized
, why is it still needed volatile
?
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}