Interview essential: Java volatile memory and semantic memory lock AQS visibility of [fine long article]

Let my people encountered, the calendar of things, because even if I have a little bit better, I'll be satisfied.

Mentioned volatile first thought is:

  • This variable to ensure the visibility of all threads, where "visibility" means that when a thread modifies the value of this variable, the new values ​​for the other thread is immediately learned.
  • Prohibit instruction reordering optimization.

Here you feel for volatile understand yet?

If you understand, we consider such a question: ReentrantLock (or other lock-based implementation AQS) is how to ensure the code snippet variables (variables mainly refers to the shared variable, there are variables competition issues) visibility?

private static ReentrantLock reentrantLock = new ReentrantLock();
private static intcount = 0;
//...
// 多线程 run 如下代码
reentrantLock.lock();
try
{
    count++;
} 
finally
{
    reentrantLock.unlock();
}
//...
复制代码

As mentioned visibility, it would first be familiar with a few concepts:

1, Jnn

JMM: Java Memory Model the Java Memory Model

The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory.

It describes the relationship between variables in a program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system.

It does this in a way that can be implemented correctly using a wide variety of hardware and a wide variety of compiler optimizations.

The main objective of the Java memory model program access rules defined for each variable, i.e., stored in the memory and the underlying details of such variables will be removed from the memory in a virtual machine variables. Variable here mainly refers to shared variables, there are variables competition issues. Java memory model requires all variables are stored in the main memory, and each thread also has its own working memory, working memory thread stored in the main memory to use a copy of a copy of the thread variables, all operating threads of the variables (reading, assignment, etc.) must be in a working memory, and not directly read and write main memory variables ( in accordance with the Java virtual machine specification, still has a copy of the variable volatile shared memory, but because of its special operation sequence provisions - before read and write data from the working memory, the data must first synchronize main memory into the working memory, all look like read and write access in the main memory directly general, the description herein is no exception to the volatile ). Between different threads can not directly access the working memory of the other variables, which are worth passing between threads need to be done by the main memory.

2, reordering

When executing the program, in order to improve performance, often have compiler and processor instructions do reordering. Reordering divided into three types:

  • Compiler optimization reordering. The compiler in single-threaded programs without changing the semantics of the premise, you can rearrange the order of execution of the statement.
  • ILP reordering. Modern processors employ instruction-level parallelism (Instruction-Level Parallelism, ILP) to overlap the plurality of instructions executed. If no data dependency exists, the processor may change the execution of machine instructions corresponding to the statement sequence.
  • Reordering the system's memory. Since the processor uses the cache and the read / write buffer, which makes the load and store operations are executed may appear out of order.

Java source code into the final sequence of instructions actually executed, will undergo the following three kinds were reordering:

For the compiler to compile JMM's attention sorting rules prohibit a particular type of compiler thinks highly of the sort (not all compilers have discouraged the sort prohibited). For reordering processor, processor JMM reordering rules claim Java compiler when generating a sequence of instructions, a particular type of memory barrier insert (Memory Barriers, Intel called Memory Fence) instruction to disable a particular type of memory barrier instructions by processor reordering.

JMM belong to language-level memory model, it ensures on different compilers and different processor platforms, discouraged by ordering and reordering processor compiler prohibit certain types of memory provide consistent visibility guarantee for programmers.

3、happens-before

  • Program sequence rules: each operation a thread, happens-before any subsequent operation to the thread.
  • Monitor lock rule: a lock to unlock, happens-before subsequent lock on the lock.
  • volatile variable rules: write to a volatile field, happens-before reading this volatile region in any follow-up. (Reading of a volatile variable, always able to see any thread] [last write this volatile variable)
  • Transitive: if A happens-before B, and B happens-before C, then A happens-before C.

Has a happens-before relationship between the two operations, does not mean that a pre-operation must be performed prior to an operation after! Before it happens-before requires only one operation (executed result) after the operation of a visible and sequenced previous operation before the second operation (the first is visible to and ordered before the second).

4, memory barrier

  • Hardware memory barrier layer is divided into two: Load Barrier and Store Barrier that is read barrier and write barrier.
  • For Load Barrier, the instruction is inserted before the Load Barrier, allows data to the cache fails, force the new load data from main memory;
  • For Store Barrier, the insert after the instruction Store Barrier, make written update to the latest data written to the primary cache memory, so that other threads are visible.
  • Memory barrier has two effects:
    • Both sides of the barrier instruction prevents reordering;
    • Forced to write data buffers / cache write-back to main memory, so that the corresponding data in the cache failure.

5, volatile memory semantics

JSR-133 from the start (i.e. start from JDK5), volatile variable write - read communication between threads can be achieved.

From the point of view of semantic memory, volatile write - and read the lock release - get the same memory effect :

  • volatile write and lock release have the same memory semantics;
  • volatile read lock acquisition has the same memory semantics.

to ensure that only a single volatile volatile variables read / write atomic property, and performing mutual exclusion lock characteristics may have to ensure the implementation of the entire atomic critical section of code. Functionally, the lock is more powerful than volatile; on the scalability and execution performance, volatile advantage.

volatile variable itself has the following characteristics:

  • Visibility. Reading of a volatile variable, always able to see (any thread) last write this volatile variable.
  • Atomicity: reading of any single variable volatile / write are atomic, even 64-bit long and double-type variable, a variable long as it is volatile, read / write the variable to be atomic. If a plurality of operations or similar volatile ++ volatile compound such operations, which do not have atomic whole.

volatile memory write semantics and volatile read:

  • A thread write a volatile variable, it is essentially a thread A issued (which is made of shared variables modified) message to the next will have to read this volatile variables a thread.
  • Thread B reads a volatile variable, in essence, thread B received (before writing this volatile variable changes made to the shared variable) message sent by a thread before.
  • A thread is a volatile variable to write, then thread B reads the volatile variables, the process is essentially the thread A sends a message to the thread B through the main memory.

JMM compiler developed for the volatile heavy collation table

  • When the second operation is a volatile time of writing, no matter what is the first operation that can not be reordered. This rule ensures that the volatile write operation before will not be compiled thinks highly volatile after ordering to write.
  • When the first operation is a volatile read when, no matter what is the second operation, it can not be reordered. This rule ensures that volatile read operation will not be compiled after ordering discouraged to read before volatile.
  • When the first write operation is a volatile, volatile second operation is a read, no reordering.

In order to achieve volatile memory semantics, the compiler when generating bytecode will be inserted in the instruction sequence memory barrier to inhibit a particular type of processor reordering. To the compiler, we found an optimum arrangement to minimize the insertion barrier is almost impossible. To this end, JMM take a conservative strategy. The following is based on a conservative strategy JMM memory barrier of insertion strategy.

  • Each preceding write operation to insert a volatile StoreStore barrier.
  • Write back operation is inserted in each of a volatile StoreLoad barrier.
  • Inserting a volatile LoadLoad barrier after each read operation.
  • Inserting a volatile LoadStore barrier after each read operation.

LoadLoad Barrier: For a statement such Load1; LoadLoad; Load2, Load2 and before subsequent data read operations to be read is accessed, the data Load1 guaranteed to be read is read is completed.

StoreStore Barrier: For a statement such Store1; StoreStore; Store2, and before the subsequent write operation is performed store2 ensure Store1 write operation visible to other processors. LoadStore Barrier: For a statement such Load1; LoadStore; Store2, and before the subsequent write operation is store2 brush, to ensure that the data to be read is read Load1 completed.

StoreLoad Barrier: For a statement such Store1; StoreLoad; Load2, before Load2 and all subsequent read operations performed to ensure Store1 writing visible to all processors. It cost are the four biggest barrier. In the implementation of most processors, this barrier is a universal barrier, both the other three memory barrier function. Insert memory barrier above strategy is very conservative, but it can ensure that any processor platform, any program can get correct volatile memory semantics.

The following is a conservative strategy, volatile write command into the memory barriers generated after the sequence of FIG.

FIG StoreStore barrier may ensure that volatile before writing, which writes all common front of any of the processors already visible. This is because StoreStore barrier to protect above all the ordinary write flushed to main memory before the volatile write.

Here is more interesting, volatile write StoreLoad barrier behind. The effect of this barrier is to avoid volatile write back and may have volatile read / write reordering. Because the compiler often can not accurately determine whether a volatile write back StoreLoad need to insert a barrier (for example, a volatile write method immediately after the return). In order to ensure to achieve volatile memory semantically correct, JMM taking a conservative strategy: In each volatile write back, or insert a StoreLoad barrier in front of each volatile read. From the perspective of the overall efficiency considerations, JMM chose the later insertion in every volatile write a StoreLoad barrier. Because the volatile write - common usage pattern is read memory semantics: a writer thread to write volatile variables, multiple reader threads reading the same volatile variable. When the number of threads to read much more than write a thread, select Insert StoreLoad barrier after volatile write will bring considerable efficiency improvement. From here you can see a feature implemented on JMM: First ensure the correctness, then go to the pursuit of efficiency.

The following is the conservative strategy, volatile read instruction memory barriers generated after insertion sequence diagram:

FIG. LoadLoad barrier to disable the processor to read the above and below normal volatile read reordering. LoadStore barrier to disable the processor to read the above and below normal volatile write reordering.

Above volatile and volatile read-write memory barrier insertion strategy is very conservative. In the actual implementation, without altering the volatile write - read memory semantics, the compiler unnecessary barrier may be omitted depending on the circumstances.

6、AQS

For AQS need to know so few points:

  • Lock state is represented by volatile int state.
  • Not obtain the lock thread queue waiting to enter the AQS.
  • Subclasses should override tryAcquire, tryRelease like.

Detailed AQS See: Interview essential: Java AQS implementation principles (graphic) analysis

7、ReentrantLock

A fair lock, for example, take a look at ReentrantLock acquiring the lock & release the lock key code:

/**
 * The synchronization state.
 */
private volatile int state;
/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
    return state;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);// 释放锁的最后,写volatile变量state
    return free;
}
 protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();// 获取锁的开始,首先读volatile变量state
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
}
复制代码

Fair locks in the lock release last write volatile variable state, first read this volatile variables when acquiring the lock. According happens-before after the regular volatile, and release the lock of the thread visible before writing volatile variables shared variables, read from the same volatile variable in acquiring the lock thread becomes immediately visible thread to acquire the lock. Thus ensuring the snippet variables (variables mainly refers to the shared variable, there are variables competition issues) visibility.

8. Summary

If we carefully analyze the source code implementation concurrent package, you will find a common realization mode.

  • First, declare shared variables as volatile.
  • Then, using the CAS atoms condition updating to achieve synchronization between threads.
  • At the same time, cooperate to volatile read / write and read and write CAS has volatile memory semantics for communication between threads.

As we mentioned earlier, the compiler will not be any memory operation reordering on volatile and volatile read-back reading; compiler does not write to volatile and volatile write arbitrary memory in front of the reordering operation. The combination of these two conditions means that in order to achieve the same time volatile read and write volatile memory semantics, the compiler can not any reordering of memory operations and CAS CAS front and back.

Reference article:

1, "Java concurrent programming art" Wei Peng Fang Tengfei with HLA - A2

2, the Java reentrant lock memory visibility analysis

Personal micro-channel public number:

Personal CSDN blog:

blog.csdn.net/jiankunking

Personal github:

github.com/jiankunking

Personal blog: www.jiankunking.com

Guess you like

Origin juejin.im/post/5d3952bff265da1b7c615dba