volatile keyword

The keyword volatile can be said to be the most lightweight synchronization mechanism provided by the Java virtual machine, but it is not easy to be fully understood correctly and completely, so that many programmers are used to not practicing it and using it. The volatile keyword is also mentioned in school, and it is not explained in depth. At work, many programmers are accustomed to using synchorized to deal with synchronization problems, and listen to many ancient beliefs "if you can't understand volatile well, don't use it", but doing so It is a way of putting the cart before the end and putting the cart before the horse. An in-depth understanding of the volatile keyword and the implementation details of the underlying virtual machine has profound significance for understanding the concurrency features of Java.

volatile is a type specifier , like the more familiar const, which is designed to modify variables that are accessed and modified by different threads . The role of volatile is as an instruction keyword to ensure that this instruction will not be omitted due to compiler optimization, and requires direct reading of the value each time.

                                                                                                                      —— Excerpted from Baidu Encyclopedia

As we all know, concurrent programming is a difficult point in programming, which can be described as a small mountain. In the Java ecosystem, the difficulty of concurrent programming is reduced by the extremely complete class libraries, but in the same way, these class libraries have also created a large number of programmers who know it but don't know why, not to mention that the class library cannot be perfectly solved. Any business requirement (the powerful class library provided by the JDK has both advantages and disadvantages). Only by knowing the support and defects of the Java virtual machine's bottom layer for Java concurrent programming can you choose the appropriate class library in the appropriate scenario, or even implement the class library yourself to solve more complex business scenarios. If you want to truly understand concurrent programming, the author believes that you might as well calm down and start from the underlying principles, because when you look at the concurrency problems of upper-layer applications from the underlying principles, the thinking will be clearer.

Why are there concurrency issues?

Apes who have a little knowledge of computers understand that the computing power of the current computer's storage device and the computing power of the processor is several orders of magnitude different. The barrel theorem: How much water it can hold is not determined by the tallest plank, but by the lowest plank. According to the barrel theorem, it is undoubtedly the current storage device of the computer that determines the computing speed and performance of the computer (to understand the computing speed between the various components of the computer, please Baidu). In order to make the computer run faster, computer pioneers introduced a layer of cache between the processor and the storage device with an operation speed very close to the processor: copy the data that the operation needs to use into the cache, so that the operation can be performed quickly. , when the operation is finished, it is synchronized from the cache back to the memory, so that the subprocessor does not have to wait for slow memory reads and writes. However, in a multiprocessor system, each processor corresponds to a cache, and these caches share the same main memory, so a new problem is introduced - cache coherency. In addition to increasing the cache, the processor will also perform out-of-order execution optimization on the input code. Similar to the processor's out-of-order execution optimization, the Java virtual machine's just-in-time compilation also has similar instruction reordering optimization.

Therefore, the source of the concurrency problem is that when multiple caches share the same main memory, if multiple caches share data (such as variables), it is possible that the same cache record may appear at a certain moment. It is worth the inconsistency of the state. In order to solve the problem of cache inconsistency, the pioneers introduced the cache coherence protocol (bus lock mechanism). The architecture diagram is as follows:

Java memory model

The Java Virtual Machine Specification attempts to define a Java memory model to shield the memory access differences of various hardware operating systems, so that Java programs can achieve consistent memory access effects on various platforms. (This design idea can be said to be ubiquitous in Java programming. The underlying implementation details are shielded through abstraction, and only the upper layer is provided with a transparent interface).

From the perspective of the Java virtual machine, the problem of memory allocation is that each Java thread is allocated its own working memory, and the working memory is not transparent, and at the same time they share the same main memory, so the problem of concurrency lies in each thread The problem of inconsistency of the data state generated during the interaction between the working memory and the main memory.

All the problems that run through Java concurrent programming revolve around the following three principles, and from point to point have produced a lot of class libraries used in different scenarios (because it is explained around the Java technology ecology, so we use The technical system of the Java ecosystem is used to analyze the three principles. The following original text is from "In-depth Understanding of the Java Virtual Machine") and the Java memory model is also built around how to deal with the three principles in the concurrent process:

Atomicity: Atoms are indivisible. The atomic variable operations directly guaranteed by the Java memory model include read, load, assign, use, store, and write. It can be roughly considered that access to basic data types is atomic (except for the non-atomic nature of long and double). agreement).

Visibility: Visibility means that when a thread modifies the value of a shared variable, other threads are immediately aware of the modification. The Java memory model achieves visibility by synchronizing the new value back to main memory after the variable is modified, and refreshing the variable from main memory before the variable is read, relying on main memory as a transfer medium. This is true for both ordinary variables and volatile variables, the difference between them is that the special rules of volatile ensure that new values ​​are immediately synchronized to main memory, and flushed from main memory immediately before each use. Note: In addition to volatile, synchorized and final can also achieve visibility. The synchorized keyword is used to ensure visibility in the form of synchronization, while final is: once the field modified by fianl is initialized in the constructor, and the constructor does not Without passing the reference to "this" (the this reference escapes), other threads can see the value of the final field.

Orderliness: The orderliness of the Java memory model was discussed in detail in the previous explanation of volatile. The natural orderliness in Java programs can be summed up in one sentence: if observed in this thread, all operations are In-order (semantics that appear to be serial within a thread), if another thread is observed in another thread, all operations are out-of-order (instruction reordering and working memory and main memory synchronization delay phenomenon). Volatile itself contains the semantics of prohibiting instruction reordering, while synchronized is "a variable only allows one thread to lock it at a time".

The Java memory model defines the particularity of volatile variables, and the sky has visibility and order in the three principles. Why do you say volatile has visibility? When a variable is declared volatile, it will have the following two characteristics:

1. Ensure the visibility of this variable to all threads. The visibility here means that when multiple threads share the variable at the same time, if one thread modifies the variable, another thread can immediately perceive its new value. (In the working memory of each thread, there is no inconsistency in volatile variables, but since it must be refreshed before each use, the execution engine cannot see the inconsistency, because it can be considered that there is no inconsistency problem). However, the Java memory model stipulates the memory visibility of volatile variables, but does not stipulate the atomicity of its operation process, so in the multi-threaded concurrent scenario, the operation of volatile is not thread-safe.

2. Disable instruction reordering optimization. Ordinary variables only guarantee that the correct results can be obtained in all places that depend on the assignment results during the execution of the method, but cannot guarantee that the sequence of assignment operations of variables is consistent with the execution sequence of the program code.

For example the following code:

Map configOptions;
char[] configText;

// 思考此变量为何一定要申明为volatile
volatile boolean initialized = false;


// 假设一下代码在线程A中执行
// 模拟读取配置文件,并将读完完载入内存后将initilized设置为true以通知其他线程配置可用。
configOptions = new HashMap<>();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initilized = true;


// 假设一下代码在线程B执行
// 等待initlized为true,代表线程A已经把配置信息初始化完成
while(!initilized) {
	sleep();
}
// 使用线程A读取的配置信息
doSomethingWithConfig();

If the execution code of thread A executes the initlized assignment operation before it has finished reading the configuration information, then thread B can use the configuration information before thread A has completed the reading operation, and the result will naturally be A major error will occur. Instruction reordering is a machine instruction optimization operation, which belongs to the system optimization level.

So how does volatile prohibit the reordering of system instructions?

When locating a variable as volatile, if you operate on a volatile variable, the Java virtual machine inserts a memory barrier instruction during its operation (assignment, operation, etc.). The role of the memory barrier instruction is to ensure that the operation and modification of the variable will be synchronized to the main memory in real time, and the cache coherence (bus lock mechanism) will use a sniffing instruction to identify the value of the variable currently recorded by other threads in the invalid state. , then when they need to access the variable, they will actively initiate an instruction to synchronize the real value of the variable from main memory.

In a simple summary, from analyzing why there is a concurrency problem, to the Java memory model, and how it solves the three principles (features) of concurrency, and then thinking about concurrency, it seems to be the same thing . Then look at the volatile variable, what the bottom layer does to it, how to ensure visibility and order, and then go back to think about the applicable scenarios of volatile and how to choose between synchorized and volatile, you can understand.

Volatile variables have multi-threaded visibility and the ability to prohibit instruction reordering, but their operations are not atomic operations, so the applicable scenarios of volatile can be simply summarized into two points:

A. The result of the operation does not depend on the current value of the variable, or it can be guaranteed that only a single thread modifies the value of the variable.

B. Variables do not need to participate in invariant constraints with other state variables.

As for how to choose between synchronized and volatile variables? The performance cost of read operations of volatile variables is almost the same as that of ordinary variables, but write operations may be slower because many memory barrier instructions need to be inserted in native code to ensure that the processor does not execute out of order. Therefore, it is recommended to give priority to volatile variables. Only when the ability of volatile is not enough to solve the business scenario, consider synchorized and APIs under the juc concurrent programming package.

But if all the orderliness needs to be done by relying on voaltile and synchorized, then many operations will become very cumbersome, and we don't notice it when we write concurrent code, because a happen-before in the Java language in principle. It serves as a norm to guide us how to judge whether there is contention in data and whether it is thread-safe. Through these rules (norms), we can solve all the problems of whether there is a conflict between two operations in a concurrent environment, no longer limited to the visibility or atomic synchronization of volatile and synchorized, and some "natural" precedence relationships under the Java memory model. , these look-ahead relationships already exist without any synchronizer assistance. . (The following principle of first occurrence is excerpted from "In-depth Understanding of Java Virtual Machine")

  • Program order rules. Within a thread, according to the program code order, the operations written in the front occur before the operations written in the back.
  • Monitor locking rules. An unlock operation occurs before a subsequent lock operation on the same lock. The same lock, and the sequence in time.
  • volatile variable rules.
  • Thread start rules: The start method of the thread object occurs first for every action of this thread.
  • Thread termination rules. All operations in the thread occur first in the termination detection of this thread. We can detect that the thread has terminated execution by means of the end of the thread.join() method and the return value of thread.isAlive().
  • Thread interruption rules. The call to the thread interrupt() method detects the occurrence of the interrupt time before the code that occurs in the interrupted thread, which can be detected by the threead.interrupt() method.
  • Object termination rules. The completion of initialization of an object (the end of constructor execution) happens first at the beginning of its finalize() method.
  • transitivity. If operation A occurs before operation B, and operation B occurs before operation C, then operation A occurs before operation C.

The above learning mainly comes from "In-depth Understanding of Java Virtual Machine", the author Zhou Zhiming Zhou, wrote very, very good, very recommended.

 

 

 

 

 

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325484446&siteId=291194637