Deep parsing of Java Memory Model: Lock — turn

Original address: http://www.codeceo.com/article/java-memory-5.html

Release of lock - acquire the established happens before relationship

Lock is the most important synchronization mechanism in Java Concurrent Programming. In addition to making the critical area mutually exclusive, locks can also make the thread that releases the lock send messages to the thread that obtains the same lock.

The following is an example code of lock release acquisition:

<span class="hljs-function"&gt;<span class="hljs-keyword"&gt;public <span class="hljs-keyword"&gt;synchronized <span class="hljs-keyword"&gt;void <span class="hljs-title"&gt;writer<span class="hljs-params"&gt;() {  <span class="hljs-comment"&gt;//1
    a++;                             <span class="hljs-comment"&gt;//2
}                                    <span class="hljs-comment"&gt;//3

<span class="hljs-function"&gt;<span class="hljs-keyword"&gt;public <span class="hljs-keyword"&gt;synchronized <span class="hljs-keyword"&gt;void <span class="hljs-title"&gt;reader<span class="hljs-params"&gt;() {  <span class="hljs-comment"&gt;//4
    <span class="hljs-keyword"&gt;int i = a;                       <span class="hljs-comment"&gt;//5
    ……
}                                    <span class="hljs-comment"&gt;//6

}

Suppose thread a executes the writer () method, and then thread B executes the reader () method. According to the happens before rule, the happens before relationships included in this process can be divided into two categories:

The graphical expression of the above happens before relationship is as follows:

In the figure above, the two nodes linked by each arrow represent a happens before relationship. Black arrows indicate program sequence rules; The orange arrow indicates the monitor lock rule; The blue arrow indicates the happens before guarantee provided after combining these rules.

The above figure shows that after thread a releases the lock, thread B acquires the same lock. In the above figure, 2 happens before 5. Therefore, all visible shared variables of thread a before releasing the lock will become visible to thread B immediately after thread B obtains the same lock.

Memory semantics for lock release and acquisition

When a thread releases the lock, JMM will refresh the shared variables in the local memory corresponding to the thread into the main memory. Take the above monitorexample program as an example. After thread a releases the lock, the state diagram of shared data is as follows:

When a thread acquires a lock, JMM will set the local memory corresponding to the thread as invalid. Thus, the critical area code protected by the monitor must read the shared variables from the main memory. The following is the status diagram of lock acquisition:

Comparing the memory semantics of lock release acquisition with that of write read, we can see that lock release has the same memory semantics as volatile write; Lock acquisition has the same memory semantics as volatile read.

The following is a summary of the memory semantics of lock release and lock acquisition:

Implementation of memory locking semantics

This paper will analyze the specific implementation mechanism of lock memory semantics with the help of reentrantlock source code.

See the following example code:

<span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">void <span class="hljs-title">writer(<span class="hljs-params">) {
<span class="hljs-keyword">lock.<span class="hljs-keyword">lock(); <span class="hljs-comment">//获取锁
<span class="hljs-keyword">try {
a++;
} <span class="hljs-keyword">finally {
<span class="hljs-keyword">lock.unlock(); <span class="hljs-comment">//释放锁
}
}

<span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">void <span class="hljs-title">reader (<span class="hljs-params">) {
<span class="hljs-keyword">lock.<span class="hljs-keyword">lock(); <span class="hljs-comment">//获取锁
<span class="hljs-keyword">try {
<span class="hljs-keyword">int i = a;
……
} <span class="hljs-keyword">finally {
<span class="hljs-keyword">lock.unlock(); <span class="hljs-comment">//释放锁
}
}
}

In reentrantlock, you call the lock () method to get the lock. Call the unlock () method to release the lock.

The implementation of reentrantlock depends on the Java synchronizer framework abstractqueuedsynchronizer (AQS for short). AQS uses an integer volatile variable (named state) to maintain the synchronization state. We will see that this volatile variable is the key to the memory semantic implementation of reentrantlock. The following is the class diagram of reentrantlock (draw only the parts related to this article):

Reentrantlock is divided into fair lock and unfair lock. We first analyze fair lock.

When a fair lock is used, the method call trace of the lock () method is as follows:

Start locking in step 4. Here is the source code of the method:

获取锁的开始,首先读volatile变量state
    

From the above source code, we can see that the locking method first reads the volatile variable state.

When using a fair lock, the method call track of unlock () is as follows:

In step 3, you really start to release the lock. Here is the source code of this method:

From the above source code, we can see that the volatile variable state is written at the end of releasing the lock.

The fair lock writes the volatile variable state at the end of releasing the lock; The volatile variable is read first when the lock is acquired. According to the happens before rule of volatile, the shared variable visible to the thread releasing the lock before writing the volatile variable will become visible to the thread acquiring the lock immediately after the thread acquiring the lock reads the same volatile variable.

Now we analyze the implementation of memory semantics of unfair locks.

The release of unfair locks is exactly the same as that of fair locks, so here we only analyze the acquisition of unfair locks.

When a fair lock is used, the method call trace of the lock () method is as follows:

Start locking in step 3. Here is the source code of the method:

This method updates the state variable in the form of atomic operation. This paper calls the compareandset () method of Java as CAS for short. The JDK document describes this method as follows: if the current state value is equal to the expected value, set the synchronization state to the given update value atomically. This operation has memory semantics of volatile read and write.

Here, we analyze how CAS has memory semantics of volatile read and volatile write from the perspective of compiler and processor respectively.

As mentioned earlier, the compiler will not reorder volatile reads and any memory operations after volatile reads; The compiler does not reorder volatile writes and any memory operations preceding volatile writes. Combining these two conditions means that in order to realize the memory semantics of volatile read and volatile write at the same time, the compiler cannot reorder any memory operations before and after CAS.

Let's analyze how CAS has both volatile read and volatile write memory semantics in common Intel x86 processors.

Here is sun misc. Source code of compareandswapint() method of unsafe class:

You can see that this is a local method call. The C + + code that this local method calls in turn in openjdk is: unsafe cpp,atomic. CPP and atomic windows x86 inline. hpp。 The final implementation of this local method is in the following location of openjdk: openjdk-7-fcs-src-b147-27jun2011 \ openjdk \ hotspot \ SRC \ oscpu \ windowsx86 \ VM \ atomicwindowsx86 inline. HPP (corresponding to Windows operating system, x86 processor). The following is a fragment of the source code corresponding to Intel x86 processor:

Meta">#define LOCK_IF_MP(mp) __asm cmp mp,0  \
                       __<span class="hljs-keyword">inline jint     Atomic::cmpxchg    (jint     exchange_value,<span class="hljs-keyword">volatile jint*     dest,jint     compare_value) {
<span class="hljs-comment">// alternative for InterlockedCompareExchange
<span class="hljs-keyword">int mp = os::is_MP();
__<span class="hljs-keyword">asm {
mov edx,dest
mov ecx,exchange_value
mov eax,compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx],ecx
}
}

As shown in the above source code, the program will decide whether to add lock prefix to cmpxchg instruction according to the type of current processor. If the program runs on a multiprocessor, add the lock prefix (lock cmpxchg) to the cmpxchg instruction. On the contrary, if the program runs on a single processor, omit the lock prefix (the single processor itself will maintain the order consistency within the single processor and does not need the memory barrier effect provided by the lock prefix).

The Intel manual describes the lock prefix as follows:

The memory barrier effect of points 2 and 3 above is sufficient to realize the memory semantics of volatile read and volatile write at the same time.

After the above analysis, we can finally understand why the JDK document says that CAS has both volatile read and volatile write memory semantics.

Now let's summarize the memory semantics of fair locks and unfair locks:

From the analysis of reentrantlock in this paper, we can see that there are at least two ways to realize the memory semantics of lock release acquisition:

Implementation of concurrent package

Since Java CAS has both volatile read and volatile write memory semantics, there are four ways to communicate between Java threads:

Java CAS uses efficient machine level atomic instructions provided on modern processors, which perform read change write operations on memory in an atomic manner, This is the key to synchronization in multiprocessors (in essence, a computing machine that can support atomic read change write instructions is an asynchronous equivalent machine for sequential computing Turing machines. Therefore, any modern multiprocessor will support some atomic instructions that can perform atomic read change write operations on memory). At the same time, the read / write of volatile variables and CAS can realize the communication between threads. These features are integrated into one Since then, it has formed the cornerstone of the implementation of the entire concurrent package. If we carefully analyze the source code implementation of concurrent package, we will find a general implementation mode:

AQS, non blocking data structure and atomic variable classes (classes in java.util.concurrent.atomic package). The basic classes in these concurrent packages are implemented using this mode, and the high-level classes in the concurrent package depend on these basic classes. Overall, the implementation diagram of the concurrent package is as follows:

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>