Recommended reading

  • An in-depth introduction to the concepts of concurrent programming models

  • An in-depth rearrangement prelude to the concurrent programming model

  • An in-depth tutorial on sequential consistency in concurrent programming models


Before reading this article, it is recommended to read the concepts of the Concurrent programming model in depth to understand reordering, memory barriers, and happens-before. Otherwise this is going to be a little hard to read.

visibility

The results of one thread’s operations become visible to other threads

  • Volatile: Ensures visibility of writes to variables
  • Synchronized: locks the read and write of variables (or code blocks). After the execution is complete, the operation result is written back to the memory to ensure the visibility of the operation

How is volatite used to ensure visibility

When using the volatite variable in Java, it is guaranteed to be effective. The working principle is as follows:

The LOCK prefix directive is used in combination with the MESI protocol

For volatile variables, the JVM sends a lock prefix to the CPU, which immediately writes the value back to main memory, and because of the MESI cache consistency protocol, each CPU sniffs the bus for changes in its local cache. If the value of a cache is found to have changed, the CPU will expire its local cache, and the thread executing on that CPU will reload the latest data from main memory when it reads that variable.

The combination of the LOCK prefix directive and the MESI protocol ensures visibility.

How does synchronized guarantee visibility

Synchronized mainly locks variable reads and writes, or the execution of code blocks. Before releasing the lock, other synchronized modified variables or code blocks cannot be operated on site. In addition, the value of the variable is written to memory before the lock is released, and the new value is read by other threads when they come in.

atomic

Atomicity means that no other operations are allowed during the execution of a single operation until the operation is complete.

In Java, assignments to variables of primitive data types are atomic operations. But it is not atomic for compound operations, such as:

int a = 0; // a++; Int b = a; int b = a; int b = a; // This is not atomic, first read a, then set b to aCopy the code

In the Java JMM model, eight atomic operations are defined:

  • Lock: Acts on a variable in memory to identify it as the exclusive state of a thread

  • Unlock: Applies to a variable in memory. It frees a variable from its exclusive state in one thread to its exclusive state in another field

  • Read: Reads variables from memory into the thread’s working memory for use by the LOAD operation

  • Load: Acts on thread working memory and stores variables read from memory to a copy of the variables in working memory

  • Use: Applies to a variable in working memory. This action is required when the virtual machine executes bytecode that requires the variable

  • Assign: Applies to a variable in working memory. This operation is performed when the VM executes the bytecode assignment for the variable to assign the value to the variable in working memory

  • Store: Acts on variables in working memory, passing variables in working memory to memory

  • Write: an in-memory variable that writes variables passed in the Store step to memory

order

Sequence of program execution The sequence of code execution steps

  • Valotile: Ensures order by disallowing instruction reordering
  • Synchronized: ensures visibility by locking and mutually exclusive execution of other threads

In Java, the processor and compiler reorder instructions. However, this reordering only has no effect on the execution results of programs in a single thread, which can be affected in a multithreaded environment.

int a = 10; // 1 int b = 12; // 2 a = a + 1; // 3 b = b * 2; / / 4Copy the code

In fact, in a single-threaded environment, the order of the procedures 1 and 2 have no effect on the result of the program, the program execution order 3 and 4 have no effect on the result of the program execution, they are can do it under the optimization of compiler or handling instructions rearrangement, but the program will not perform before program 1, 3 because it will affect the program execution result. For instructions on reordering, I recommend reading the reordering preamble for an in-depth explanation of the concurrent programming model.

boolean flag = true; 
flag = false; // 0 int a = 0; // thread 1 executes code 1 and 2 a = 1 // flag = 1true; Thread 2 executes code 3, 4, 5, 6while(! flag){ // 3 a = a + 1; // 4 } // 5 System.out.println(a); / / 6Copy the code

At this point, if there are two live implementations of the code, the logic we wrote is to perform 1 and 2, then 3, 4, 5, 6, and 7. But in a multithreaded environment, if the instructions are reordered so that 2 is executed before 0, then the expected output a is 2, which is actually 1.

Therefore, in Java, we need to use Valotite, synchronized to protect the program, prevent instruction reordering program output is not the expected result.

Important principles to ensure order

In Java, when the compiler and processor want to reorder instructions, reordering does not happen if the program follows the following principles, which the JMM mandates.

Happens-before Four principles

  • Sequence rule: Within a thread (not multithreading), in order of code, actions written earlier take place before actions written later

  • Monitor lock rule: Unlocking a monitor occurs before owning the same lock

  • Volatile variable rules: Writes to a variable occur first before reads to that variable occur later. That is, if the program code is written before it is read, it cannot be reordered read before it is written.

  • Transfer rule: If operation A precedes operation B and operation B precedes operation C, it follows that operation A precedes operation C

If the program does not meet these four principles, in principle it can be arbitrarily reordered.

How is volatility used to ensure order

The memory barrier
LoadLoad Memory barrier

Load corresponds to the JMM meaning of loading data.

Syntax format:

// load1 indicates to load instruction 1, load2 indicates to load instruction 2 load1: LoadLoad :load2Copy the code

LoadLoad barrier: load1; LoadLoad; Load2 ensures that load1 data is loaded before all load2 instructions, that is, the code corresponding to load1 and the code corresponding to load2 cannot be rearranged

StoreStore Memory barrier

Store corresponds to the JMM meaning of storing data in thread-local working memory

Syntax format:

Store1; StoreStore; store2Copy the code

StoreStore barrier: store1; StoreStore; Store2, ensuring that data from store1 is always flushed back to main memory and visible to other cpus before store2 and subsequent instructions

LoadStore Memory barrier

Syntax format:

Load1; LoadStore; store2Copy the code

LoadStore barrier: load1; LoadStore; Store2, ensure that the load1 instruction loads data before store2 and subsequent instructions

StoreLoad Memory barrier

Syntax format:

Store1; LoadStore; load2Copy the code

StoreLoad barrier: store1; StoreLoad; Load2, to ensure that the data of store1 instructions must be flushed back to main memory and visible to other cpus before the data of load2 and subsequent instructions is loaded

How are volatile variables represented in the memory barrier?

Take a look at this code:

volatile a = 1; a = 2; Store operation int b = a // load operationCopy the code

Memory barriers are added to read and write operations on volatile variables

  • A LoadLoad barrier is placed in front of each volatile read to prevent normal and voaltile read rearrangements
  • After each volatile read, a LoadStore barrier is added to prevent the following common writes and volatile read rearrangements
  • Each volatile write is preceded by a StoreStore barrier that disallows the common write above and its rearrangement
  • After each volatile write, a StoreLoad barrier is added to prevent reordering of volatile reads/writes.

So, the pseudoinstruction code for the above code:

volatile a = 1; // Declare an a variable with the value 1 StoreStore; // disallow a= 1 and a=2; StoreLoad; Int b = a; // Make sure that a is flushed back to main memory and visible to all cpus.Copy the code

conclusion

Here and we detailed analysis of concurrency three characteristics of the problem, respectively is effective, atomic and ordered, and in Java how to ensure these three characteristics, the specific principle is what.

Recommended reading

  • An in-depth introduction to the concepts of concurrent programming models

  • An in-depth rearrangement prelude to the concurrent programming model

  • An in-depth tutorial on sequential consistency in concurrent programming models