Java concurrent programming: volatile keyword Parsing

Source: Internet
Author: User

Java concurrent programming: volatile keyword Parsing
The volatile keyword may have been heard or used by many friends. Before Java 5, it was a controversial keyword, because using it in a program often leads to unexpected results. After Java 5, the volatile keyword can be revived. Although volatile keywords are relatively simple to understand literally, it is not easy to use them well. Because the volatile keyword is related to the Java memory model, before describing the volatile key, let's take a look at the concepts and knowledge related to the memory model, then, it analyzes the implementation principle of the volatile keyword, and finally provides several scenarios for using the volatile keyword. The following is the directory outline of this article: 1. concept 2 of the Memory Model. three Concepts in concurrent programming 3. java Memory Model 4 .. in-depth analysis of volatile keyword 5. if the volatile keyword is incorrect, please forgive me and criticize and correct it. Please respect the author's labor results, reproduced please indicate the original link: 1. we all know the concept of the memory model. When a computer executes a program, each instruction is executed in the CPU. However, the instruction execution process will inevitably involve data reading and writing. Because temporary data during the program running is stored in the primary memory (physical memory), there is a problem, because the CPU execution speed is very fast, the process of reading data from the memory and writing data to the memory is much slower than that of executing commands by the CPU, therefore, data operations must be performed through interaction with the memory at any time, which greatly reduces the speed of instruction execution. Therefore, there is a high-speed cache in the CPU. That is, when the program is running, it will copy the data required by the operation from the primary storage to the high-speed cache of the CPU, the CPU can directly read data from its cache and write data to it during computation. After the computation, it refresh the data in the cache to the primary storage. For example, the following code: 1i = I + 1; when the thread executes this statement, it first reads the I value from the main memory, copy a copy to the high-speed cache, then the CPU executes the command to add 1 to the I, and then write the data to the high-speed cache, finally, refresh the latest I value in the cache to the primary storage. This code runs in a single thread without any problems. In multi-core CPUs, each thread may run on different CPUs, so each thread has its own high-speed cache (this problem also occurs for Single-core CPUs, it is executed separately in the form of Thread Scheduling ). This document uses multi-core CPU as an example. For example, if there are two threads executing this code at the same time, if the initial I value is 0, then we want the I value to be 2 after the two threads are executed. But is that true? There may be the following situation: Initially, the two threads read the I value separately and store it in the cache of their respective CPUs. Then thread 1 is added to the cache, then write the latest value 1 of I to the memory. In this case, the I value in the cache of thread 2 is still 0. After adding 1, the I value is 1, and thread 2 writes the I value to the memory. The final result I is 1 instead of 2. This is the famous cache consistency problem. It is usually called a shared variable that is accessed by multiple threads. That is to say, if a variable is cached in multiple CPUs (usually during multi-threaded programming), there may be cache inconsistencies. To solve the cache inconsistency problem, there are usually two solutions: 1) Add LOCK # LOCK on the bus; 2) both methods of cache consistency Protocol are provided on the hardware layer. In the early stages, the cache inconsistency was solved by adding a LOCK # LOCK to the bus. Because the CPU communicates with other components through the bus, if you LOCK the bus, that is to say, other CPUs are blocked from accessing other components (such as memory ), so that only one CPU can use the memory of this variable. For example, in the above example, if a thread is executing I = I + 1, if the LCOK # Lock signal is sent on the bus during the execution of this code, then, only after the Code is fully executed can other CPUs read the variable from the memory where variable I is located and perform corresponding operations. In this way, the cache inconsistency problem is solved. However, the above method may cause a problem because other CPUs cannot access the memory during the bus lock, resulting in low efficiency. So there is a cache consistency protocol. Intel's MESI protocol ensures that copies of shared variables used in each cache are consistent. Its core idea is: when the CPU writes data, if the operation variable is a shared variable, that is, a copy of the variable also exists in other CPUs, the system sends a signal to other CPUs to invalidate the cached rows of the variable. Therefore, when other CPUs need to read the variable, if you find that the cached row of the variable in your cache is invalid, it will be re-read from the memory. Ii. Three Concepts in concurrency programming we usually encounter the following three problems in concurrency programming: atomicity, visibility, and orderliness. Let's take a look at these three concepts first: 1. atomicity: one or more operations are either performed in full and the execution process is not interrupted by any factors, or they are not executed. A classic example is the bank account transfer problem. For example, if you transfer RMB 1000 from account A to account B, two operations are required: deduct RMB 1000 from account, add 1000 yuan to account B. Think about the consequences of these two operations if they are not atomic. Assume that the operation is terminated suddenly after account A is subtracted from 1000 RMB. Then, 500 yuan was taken out from B and 500 yuan was taken out. Then, the Account B was added with 1000 yuan. In this way, although account A loses 1000 yuan, Account B does not receive the transferred 1000 yuan. Therefore, these two operations must be atomic to avoid unexpected problems. What results will be reflected in concurrent programming? For example, if the assignment process for a 32-bit variable is not atomic, what will happen? 1i = 9; if a thread executes this statement, I assume that a 32-bit variable has two procedures: assign a value to a low 16-bit variable and assign a value to a high 16-bit variable. A possible situation occurs: When a low 16-bit value is written, it is suddenly interrupted, and another thread reads the I value, the read data is the error data. 2. Visibility refers to the value of this variable modified by one thread when multiple threads access the same variable. Other threads can immediately see the modified value. For a simple example, see the following code: // code executed in thread 1 int I = 0; I = 10; // code executed in thread 2 j = I; if the execution thread 1 is CPU1, the execution thread 2 is CPU2. According to the above analysis, when thread 1 executes the I = 10 sentence, the initial value of I is first loaded into the cache of CPU1, and then assigned a value of 10, in the cache of CPU1, the I value becomes 10, but it is not immediately written to the primary storage. In this case, thread 2 executes j = I. It first reads the I value from the primary storage and loads it into the cache of CPU2. Note that the I value in the memory is still 0, then, the j value is 0 instead of 10. this is the visibility issue. After thread 1 modifies the variable I, thread 2 does not immediately see the value modified by thread 1. 3. orderliness: The program execution order is executed in the order of code. For a simple example, see the following code: int I = 0; boolean flag = false; I = 1; // Statement 1 flag = true; // Statement 2 the code above defines an int type variable, defines a boolean type variable, and then assigns values to the two variables respectively. In terms of code sequence, Statement 1 is prior to Statement 2. Will JVM ensure that statement 1 will be executed before Statement 2 when executing this code? Not necessarily. Why? Here, commands may be re-ordered (Instruction Reorder ). What is Command Re-sorting? Generally, the processor may optimize the input code to improve the program running efficiency, it does not guarantee that the execution sequence of each statement in the program is the same as that in the code, but it ensures that the final execution result of the program is consistent with that of the Code. For example, in the code above, the first execution of statements 1 and 2 has no impact on the final program results, so it is possible that during the execution, statement 2 is executed first and Statement 1 is executed later. However, it should be noted that although the processor will re-sort the commands, it will ensure that the final result of the program will be the same as the code execution result. What guarantee does it rely on? Let's look at the following example: int a = 10; // statement 1int r = 2; // statement 2a = a + 3; // statement 3r = a *; // Statement 4 the Code contains four statements. The execution sequence may be: this execution sequence is not possible: Statement 2 Statement 1 Statement 4 Statement 3 is not possible, because the processor will consider the data dependency between commands during re-sorting, if an Instruction 2 must use the result of Instruction 1, the processor will ensure that Instruction 1 will be executed before Instruction 2. Although re-sorting does not affect the execution results of a single program in a single thread, what about multithreading? The following is an example: // thread 1: context = loadContext (); // Statement 1 inited = true; // Statement 2 // thread 2: while (! Inited) {sleep ()} doSomethingwithconfig (context); In the code above, because statements 1 and 2 do not have data dependencies, they may be reordered. If re-sorting occurs, Statement 2 is first executed during thread 1 execution, and thread 2 will think that the initialization is completed, then the while loop will jump out, execute the doSomethingwithconfig (context) method. If the context is not initialized, an error occurs. It can be seen from the above that the command re-sorting will not affect the execution of a single thread, but will affect the correctness of concurrent thread execution. That is to say, to correctly execute a concurrent program, you must ensure atomicity, visibility, and orderliness. If one is not guaranteed, the program may run incorrectly. Iii. Some problems may occur in the memory model and concurrent programming in the Java memory model. Let's take a look at the Java memory model, we will study what guarantees the Java memory model provides for us and what methods and mechanisms are provided in java to ensure the correctness of program execution during multi-threaded programming. In the Java Virtual Machine specification, we try to define a Java Memory Model (JMM) to avoid Memory access differences between hardware platforms and operating systems, to achieve consistent memory access performance for Java programs on various platforms. So what does the Java Memory Model define? It defines the access rules for variables in the program, and, to a larger extent, defines the execution order of the program. Note that for better execution performance, the Java memory model does not limit the execution engine to use processor registers or high-speed caches to speed up instruction execution, nor limit the compiler to re-Sort instructions. That is to say, in the java memory model, there will also be cache consistency problems and Command Re-sorting problems. The Java memory model requires that all variables exist in the primary memory (similar to the physical memory mentioned above), and each thread has its own working memory (similar to the previous high-speed cache ). All operations on variables by the thread must be performed in the working memory, rather than directly performing operations on the main memory. In addition, each thread cannot access the working memory of other threads. For example, in java, execute the following statement: 1i = 10; the execution thread must first assign values to the cache row where variable I is located in its own working thread, and then write it to the primary storage. Instead of Directly Writing the value 10 to the primary storage. What guarantee does Java provide for Atomicity, visibility, and orderliness? 1. Atomicity in Java, reading and assigning values to variables of the basic data type are atomic operations, that is, these operations cannot be interrupted, either executed or not executed. Although the above sentence seems simple, it is not so easy to understand. Take the following example I: Which of the following operations are atomic? x = 10; // statement 1y = x; // statement 2x ++; // statement 3x = x + 1; // Statement 4 some may say that the operations in the above four statements are atomic operations. In fact, only statement 1 is an atomic operation, and the other three statements are not atomic operations. Statement 1 directly assigns the value 10 to x. That is to say, when the thread executes this statement, it directly writes the value 10 to the working memory. Statement 2 actually contains two operations. It first reads the value of x and then writes the value of x to the working memory, although reading the value of x and writing the value of x to the working memory are atomic operations, they are not atomic operations. Similarly, x ++ and x = x + 1 include three operations: Read the value of x, add 1, and write new values. Therefore, only the operations of Statement 1 in the preceding four statements are atomic. That is to say, only simple reading and assigning values (and must assign values to a variable, and mutual assigning values between variables is not an atomic operation) is an atomic operation. However, it is worth noting that on a 32-bit platform, reading and assigning values to 64-bit data requires two operations, which cannot guarantee the atomicity. However, in the latest JDK, JVM has ensured that reading and assigning values to 64-bit data is also atomic. As can be seen from the above, the Java memory model only ensures that the basic read and value assignment are atomic operations. To achieve the atomicity of a larger range of operations, you can achieve it through synchronized and Lock. Because synchronized and Lock can ensure that only one thread executes the code block at any time, there is no atomic problem, thus ensuring atomicity. 2. for visibility, Java provides the volatile keyword to ensure visibility. When a shared variable is modified by volatile, it ensures that the modified value is immediately updated to the primary storage. When other threads need to read the variable, it reads the new value from the memory. The visibility of common shared variables cannot be guaranteed, because after the common shared variables are modified, it is not clear when they are written to the primary storage. When other threads read the shared variables, in this case, the memory may still be the old value, so visibility cannot be guaranteed. In addition, synchronized and Lock can also ensure visibility. synchronized and Lock can ensure that only one thread obtains the Lock at the same time and then executes the synchronization code, before the lock is released, the modification to the variable is refreshed to the primary storage. This ensures visibility. 3. Ordering in the Java memory model, the compiler and processor are allowed to re-Sort commands. However, the re-sorting process does not affect the execution of a single thread program, but affects the correctness of concurrent execution of multiple threads. In Java, you can use the volatile keyword to ensure a certain degree of "orderliness" (the specific principle is described in the next section ). In addition, synchronized and Lock can be used to ensure orderliness. Obviously, synchronized and Lock ensure that a thread executes the synchronization code at each time point, which means that the thread executes the synchronization code in sequence, naturally, order is guaranteed. In addition, the Java memory model has some inherent "orderliness" that can be ensured without any means. This is also called the happens-before principle. If the execution order of the two operations cannot be derived from the happens-before principle, they cannot guarantee their order, and virtual machines can reorder them at will. Next we will introduce the happens-before principle in detail (first occurrence principle): Program order rule: within a thread, according to the code order, the previous operation first occurs after the operation lock rule: an unLock operation first occurs after the same lock operation volatile variable rule: the write operation on A variable first takes place before the read operation transfer rule for this variable: If operation A first occurs in operation B, and Operation B first occurs in operation C, it can be concluded that operation A first occurs in operation C Thread startup rules: the start () method of the Thread object first occurs in each action of this Thread interrupt rules: for Thread interrupt () method Invocation occurs first when the code of the interrupted Thread detects the occurrence of the interrupt event. Thread termination rules: all operations on the Thread are performed first when the Thread is terminated. We can use the Thread. join () method end, Thread. the isAlive () return value method detects that the thread has terminated the execution of the object termination rule: the initialization of an object first occurs in its fina The first eight principles of the lize () method are excerpted from the "deep understanding of Java Virtual Machine". Among the eight rules, the first four are important, and the last four are obvious. Next we will explain the first four rules: For program order rules, I understand that the execution of a piece of program code looks orderly in a single thread. Note: although this rule mentions that "the operation before writing occurs first in the operation after writing", this should be the order in which the program looks to be executed in the code order, because the virtual machine may re-sort the program code. Although re-sorting is performed, the final execution result is consistent with the execution result in the program sequence. It only performs re-sorting on commands without data dependency. Therefore, in a single thread, program execution seems to be executed in an orderly manner. In fact, this rule is used to ensure the correctness of the execution results of the program in a single thread, but cannot guarantee the correctness of the program execution in multiple threads. The second rule is also easy to understand. That is to say, whether in a single thread or multiple threads, if the same lock is locked, the lock must be released first, to continue the lock operation. The third rule is an important rule and will be highlighted later. The intuitive explanation is that if a thread writes a variable first and then a thread reads the data, the write operation will certainly occur first in the read operation. The fourth rule is actually a manifestation of the pass-through of the happens-before principle. 4. In-depth analysis of the volatile keyword many things have been mentioned earlier. In fact, they all pave the way for the volatile keyword. Then we will enter the topic. 1. the two-layer semantics of the volatile keyword. Once a shared variable (the member variable of the class and the static member variable of the class) is modified by volatile, the two-layer semantics is available: 1) this ensures the visibility when different threads operate on this variable. That is, a thread modifies the value of a variable. This new value is immediately visible to other threads. 2) Command Re-sorting is prohibited. First read a piece of code. If thread 1 is executed first and thread 2 is executed later: // thread 1 boolean stop = false; while (! Stop) {doSomething () ;}// thread 2 stop = true; this code is a typical piece of code, which may be used by many people when thread interruption occurs. But in fact, will this Code fully run correctly? Will the thread be interrupted? Not necessarily. In most cases, this code can interrupt the thread, but it may also lead to thread interruption failure (although this possibility is very small, but once this happens, it will lead to an endless loop ). The following explains why this Code may cause thread interruption. As explained above, each thread has its own working memory during running, so when thread 1 is running, copy the value of the stop variable to your working memory. So when thread 2 changes the value of the stop variable, but it has not been able to write it into the main memory, thread 2 is re-running to do other things, because thread 1 does not know the changes to the stop variable of thread 2, it will keep repeating. However, modification with volatile becomes different: first, the modified value is forcibly written to the primary storage using the volatile keyword; second, when thread 2 is modified using the volatile keyword, the cache row of the stop variable in the working memory of thread 1 is invalid (if it is reflected in the hardware layer, the corresponding cache row in the L1 or L2 cache of the CPU is invalid). Third: because the cache row of the stop variable in the working memory of thread 1 is invalid, when thread 1 reads the value of the stop variable again, it will be read from the main memory. When thread 2 modifies the stop value (of course, there are two operations, namely modifying the value in thread 2's working memory and then writing the modified value to the memory ), it will invalidate the cache row of the stop variable in the working memory of thread 1. When thread 1 reads data, it will find that its own cache row is invalid, it will wait for the primary address corresponding to the cache row to be updated, and then read the latest value from the corresponding primary memory. Then thread 1 reads the latest correct value. 2. Does volatile ensure atomicity? I know from the above that the volatile keyword ensures the operation visibility, but can volatile ensure that the operation on variables is atomic? The following is an example: 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) // ensure that all the preceding threads have finished executing the Thread. yield (); System. out. println (test. inc) ;}} what is the output result of this program? Maybe some friends think it is 10000. But in fact, running it will find that each running result is inconsistent, and it is a number smaller than 10000. Some may have questions. No, the above is the auto-increment operation on the variable inc. Because volatile ensures visibility, after auto-increment of inc in each thread, the modified value can be seen in other threads. Therefore, if 10 threads perform 1000 Operations respectively, the final inc value should be 1000*10 = 10000. There is a misunderstanding here. The volatile keyword can ensure that there is no error in visibility, but the above program fails to guarantee atomicity. Visibility can only ensure that the latest value is read each time, but volatile cannot guarantee the atomicity of variable operations. As mentioned above, the auto-increment operation is not atomic, including reading the original value of the variable, adding 1, and writing to the working memory. That is to say, the three sub-operations of the auto-increment operation may be split and executed, which may lead to the following situation: if the value of the variable inc is 10 at a time point, thread 1 performs the auto-increment operation on the variables. Thread 1 first reads the original value of the variable inc, and thread 1 is blocked. Then thread 2 performs the auto-increment operation on the variables, thread 2 also reads the original value of the variable inc, because thread 1 only reads the variable inc, but does not modify the variable, therefore, the cache row of the cache variable inc in the working memory of thread 2 will not be invalid. Therefore, thread 2 will directly read the inc value from the main memory and find that the inc value is 10, then add 1, write 11 to the working memory, and write it to the primary memory. Thread 1 then adds 1. Since the inc value has been read, note that the inc value is still 10 in the worker memory of thread 1, therefore, after thread 1 adds 1 to inc, the value of inc is 11, then 11 is written to the working memory, and finally to the main memory.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.