Today’s sharing started, please give us more advice ~
This article explores the memory semantics of reordering and memory consistency and volatile
happens-before
Happens-before is a relationship. In the JMM, if the result of an operation is visible to another operation, there must be a happens-before relationship between the two operations. Note that the two operations can be either on different threads or on the same thread.
So what’s the rule about happens-before
- Procedure order rule: For every action in a thread, any subsequent action in that thread must see the result of the previous action, so happens-before any subsequent action in that thread.
- Monitor lock rule: when a lock is unlocked, subsequent lock actions must be visible, so happens-before subsequent lock actions.
- Rule for volatile variables: Volatile implements thread visibility of variables, so operations on the variable are visible later, so happens-before any subsequent reads to the volatile field.
- Transitivity: IF B is visible to A, A can happens-before B, and if C is visible to B, B can happens-before C, then A can happens-before C.
Happens-before is simply a rule that abstracts the memory visibility provided by the JMM without having to fully understand the reordering mentioned earlier. The implementation of happens-before is that the JMM disallows reordering.
reorder
Reordering is a method by which compilers and processors reorder instruction sequences to optimize program performance. The key is to optimize performance and to note that reordering occurs on a per-thread basis because the processor executes only one thread at a time.
Data dependency
Data dependency refers to two operations that access the same variable, and at least one of them is a write operation, so the two operations have data dependencies. Therefore, data dependencies can be divided into three types according to the order of the two operations:
- Write read: to read a variable after it has been written;
- Read and write: To read a variable before writing, not modifying it, e.g., a = b; B = 1 is a read-write;
- Write after write: After writing a variable, write again.
As mentioned above, the compiler and processor need to identify data dependencies in order to prevent changes in the execution result of the operations that are reordered.
Reordering is not done if there are data dependencies, but this automatic reordering disallows only for single-thread and single-processor data dependencies, which are only considered for single-thread and single-processor data dependencies, and not for data dependencies between threads and processors.
The as – if – serial semantics
The as-if-serial syntax is used to guarantee that the execution of a single thread will not change, no matter how the sequence is reordered.
Here’s an example:
Double PI = 3.14; / / A operation
double r = 1; / / B operation
double area = pi * r * r; / / C operation
In the above three operations, generate data dependence has A and C, B and C, and the resulting were written after write data dependence, so there is no data dependency, A and B the two operations occur reordering is not in violation of the as – if – serial semantics, so these two operation allows reordering occurs, but C can’t literally happened reorder operation, Both A-happensbeforec and B-happensbeforec must be satisfied.
In general, the as-if-serial semantics protect single-threaded programs from reordering problems, allowing developers to assume that programs are executed sequentially and reordering is not a problem.
As-if-serial also allows reordering of operations that have control dependencies.
Control dependencies are: logical judgment operations, i.e., if statements, which are also an operation, specifically allow the execution of the code block inside the if before determining whether the condition of the if is True or False.
Because control depend on affects the parallelism of instruction sequences, this can perform multiple commands, just go to the command executing judgment, judgment to execute other commands, such as reducing the instruction sequence of parallelism, so simply parallel execution together, judge condition again after considering the results can be maintained, which allows the reorder.
The impact of reordering on multithreading
Reordering is for single thread, single thread reordering is not any problem, because there is the guarantee of as-if-serial semantics, but multi-thread thread reordering, combined will produce multi-thread semantic errors, the program execution results to change.
If thread A modifies A flag variable and thread B obtains it, the operation of modifying the flag variable will be advanced or delayed due to the reordering of THREAD A, and the flag variable acquired by thread B may be the one before or after modification.
Sequential consistency
Program consistency is used to describe simultaneous execution of multiple threads, as follows
- All operations in a thread must be executed in program order;
- All threads see only a single order of execution of operations, whether synchronous or not, and each operation must be atomic and visible to all threads at once.
Here’s an example:
There is one thread, A, with three operations, A1, A2, A3; The other thread, B, also has three operations, B1, B2, B3.
So during synchronization, the order of execution of the six operations of the two threads is as follows (assuming that line A executes first).
As you can see, all three operations on each thread must be executed in order.
The following is just one example of the possible order in which these two threads can execute their six operations when they are out of sync
As you can see, sequential consistency ensures that operations are executed sequentially in each thread, even in the case of unsynchronization, although the overall order is out of order.
The prerequisite for sequential consistency is that each operation must be immediately visible to any thread, so that subsequent operations are not affected and can be executed immediately.
Each operation is not immediately visible to any thread. As mentioned earlier, each thread has its own cache. The operation is performed on the cache first, and then on the main memory. And the order in which all threads see operations may be inconsistent, because reordering can occur; If it is synchronous, it may not be consistent because of reordering, but because of the as-if-serial semantics, the outside world can be treated as sequential.
Here’s a look at the difference between JMM synchronization and non-synchronization and sequential consistency:
- Synchronous programs
In sequential consistency, all operations are executed sequentially, exactly in program order, whereas in JMM it is possible to reorder code for critical sections, specifically for locked code.
This reordering can improve execution efficiency without changing the result of execution.
In general, the JMM uses compiler and processor optimizations wherever possible without changing the results of synchronized program execution.
- Asynchronous program
For programs that are not synchronized, the JMM provides minimal security by ensuring that the values read are not created out of thin air and are either the values written by previous threads or the default values (0, False, Null).
This minimal security is implemented by the JVM in the allocation of object memory. When allocating memory on the heap, the allocated memory is emptied before allocating objects on it (the two operations are atomic), which is the default when allocating objects.
For performance reasons, the JMM does not support program consistency in order not to prohibit a large number of processor and compiler optimizations, and unsynchronized programs are not only out-of-order overall, but also out-of-order within individual threads (as with synchronized programs).
Volatile visibility experiments
I have two threads open. The second thread modiates volatile variables, and the first thread constantly acquires volatile variables.
The result is a consistent stuck loop with no output from the console.
Let’s say that flag is volatile
The result: after three seconds, no more messages are being printed.
Sleep is used to flush Thread memory, so do not use thread. sleep to make a Thread acquire volatile variables twice.
The characteristics of the volatile
Volatile is the equivalent of locking and synchronizing word reads or writes of variables.
Happens-before specifies that the action that releases the lock is visible to subsequent actions that acquire the lock, so the thread that releases the lock is visible to subsequent threads that acquire the lock. Meaning that the last write of a volatile variable can be read by the thread that later acquires the lock.
32-bit operating system to operate a 64 – bit variables, is divided into 32 bit high and low 32-bit to perform, but due to the lock, will lead to the operation also has the atomicity, because the semantics of the lock decides the critical region code execution has the atomicity, namely must finish the whole block of code execution, if there is no lock, so, it is not atomic, It may be performed in two discrete steps.
Thus, volatile variables themselves have the following properties
- Atomicity: no matter how big a variable is, word reads or writes are atomic, but operations like i++ are not atomic because they are two commands.
- Visibility: The thread that operates on volatile variables can obtain changes made to them by the previous thread, meaning that the current thread always sees the last write to a volatile variable.
Volatile Memory semantics for writes and reads
Let’s first examine what dependencies need to be volatile
As mentioned earlier, there are three kinds of dependencies
- Got to write
- Writing after reading
- Write after
Volatile implements visibility, so write after is irrelevant. Read after write does not require visibility, so read after requires visibility.
Write the semantic
Volatile writes memory semantics as follows:
When a volatile variable is written, the JMM flusher the value of the shared variable from the thread’s local memory to the main memory (that is, not only the local memory is modified, but the main memory is flushed). Note that the flush is in the form of a cached line (64 bytes).
Two threads, thread A changes flag and A, flag and A are the default values
So volatile writes have two operations, and the two operations are then combined into one atomic operation.
Read the semantic
The read semantics of volatile are as follows: When a volatile variable is read, the JVM invalidates the thread’s local memory, reads the shared variable from main memory, and updates the local memory. Note: The read is invalidated, and the read is not invalidated and recapitulated.
It’s the same example, but with one more thread, THREAD B, which reads the default value at first and then reads it again.
Read and write semantic
Read and write semantics correspond to what happens when volatile variables are modified.
The read-write semantics of volatile are communication between threads, so volatile also implements communication between threads to provide visibility.
When thread A writes to A volatile variable, thread A essentially sends A message to other threads that want to manipulate the volatile variable. The message indicates that thread A has modified the volatile variable and that other threads need to retrieve it.
When thread B reads a volatile variable, thread B essentially receives a message from a previous thread (it may not have received the message, but it thinks it did), knows that the variable has changed, and needs to retrieve it.
So if A writes and B reads, it’s A communication between two threads, although it’s not very rigorous, because maybe A doesn’t write and B also reads.
The realization of the volatile
The implementation of volatile, with the acc_volatile modifier on the bytecode, and the use of memory barriers at the instruction level, has been discussed in more detail.
Memory semantic implementation of volatile
Another feature of volatile that prevents command reordering is the memory semantics of volatile.
In order to achieve volatile memory semantics, the JMM restricts reordering because reordering causes semantic changes that break communication with other threads. As mentioned earlier, there are three types of reordering, while the JMM restricts compiler reordering and processor reordering, and does not limit memory reordering.
It’s hard to tell why just by looking at the table, so let’s just look at the parts that don’t reorder.
- When the second operation is volatile write, reordering cannot occur regardless of the first operation, ensuring that operations prior to volatile write are not reordered after the write.
- When the first operation is a volatile read, no reordering can occur, regardless of the second operation. This ensures that operations after a volatile read are not reordered to those before the read.
- Reordering cannot occur when the first operation is volatile write and the second operation is volatile read.
The third is easier to understand, because volatile writes affect subsequent volatile reads. Write and read are completely different from write and write, so reordering of volatile reads and writes or volatile writes and reads is not allowed.
The key is how do you understand the first two
This is because of the read semantics of volatile. Each volatile read invalidates the cached row, requiring it to be recaptured. The cached row contains not only volatile variables but also other shared variables.
Now back to number two
If the first volatile read is followed by a common read, reordering is fine, but if the common write is followed by a common write, which may be flushed into main memory, volatile reads can be problematic.
When the first operation for volatile read, read, A second operation for volatile to form two new cache line, and each cache line variables corresponding to the same value may be different, at this time if there is A reorder, can appear inconsistent, not reordering occurs, for example, from the first time A new cache line read inside A, Read B2 from the first new cache and read A2 from the second new cache. B is not the same as B2, and A is not the same as A2, so it cannot be reordered.
Now back to number one
- When the first volatile write is performed, main memory is directly modified, affecting subsequent volatile reads, so reordering is not allowed for the second volatile read.
- If the first operation is volatile, main memory will be modified directly, which will affect other threads, and reordering will result in inconsistent results, so it is not possible to reorder volatile writes.
- If the first operation is volatile write, it can be read, but not written, because writes are later updated to main memory, and reordering can result in inconsistent results.
The next thing I want to talk about is that I don’t need to reorder
- There is no requirement for volatile to precede normal reads and writes, so reordering is possible, which of course leads to concurrency issues.
- There is only one volatile read requirement between volatile reads and normal reads. This requirement is not affected by normal reads and thus can be reordered, but can cause concurrency problems for normal reads and writes.
In order to realize the semantic memory, the compiler generates bytecode, will insert memory barrier in the sequences of instructions to prohibit certain types of processing highly sorting, which is mentioned above limit reorder type, for execution efficiency, barrier for as little as possible, but let the JMM barrier to dynamically find optimal layout is impossible, So conservative JMM memory barriers and insertion strategies are used.
- Inserting a StoreStore barrier in front of each volatile write ensures that all volatile writes are completed and flushed to main memory before volatile writes.
- Inserting a StoreLoad barrier at the end of each volatile write ensures that volatile writes must be completed before reads can be performed.
- Inserting a LoadLoad barrier at the end of each volatile read ensures that all reads are completed before volatile reads.
- Inserting a LoadStore barrier at the end of each volatile read ensures that writes must wait for volatile reads to complete before continuing.
Since the first volatile read is normal and the second volatile read is reordered, no memory barrier is required in front of the volatile read.
Today’s share has ended, please forgive and give advice!