The implementation principle of Java lock

Source: Internet
Author: User
Tags cas prev

0. Preface

Unlike synchronized, lock is written entirely in Java and is not implemented by the JVM at the level of Java.
In the Java.util.concurrent.locks package there are many implementation classes of lock, commonly used are reentrantlock, readwritelock (Implementation class Reentrantreadwritelock), Its realization all relies on the Java.util.concurrent.AbstractQueuedSynchronizer class, realizes the idea to be similar, therefore we take the Reentrantlock as the explanation entry point.

1. Reentrantlock Call procedure

After observation Reentrantlock the operation of all lock interfaces to a sync class, the class inherits the Abstractqueuedsynchronizer:

static abstract class Sync extends AbstractQueuedSynchronizer  

Sync has two sub-classes:

final static class NonfairSync extends Sync
final static class FairSync extends Sync

It is clearly defined to support fair locking and unfair locking, which by default is a non-fair lock.
Let's start with the call procedure for the Reentrant.lock () method (default non-fair lock):

These annoying template patterns make it difficult to visually see the entire invocation process, in fact, through the above call process and Abstractqueuedsynchronizer comments can be found, The majority of lock functions are abstracted in Abstractqueuedsynchronizer, and only the Tryacquire method is deferred to subclasses. The semantics of the Tryacquire method is to use a specific subclass to determine whether a request thread can obtain a lock, whether successful or not Abstractqueuedsynchronizer will process the subsequent process.

2. Lock implementation (locking)

Simply put, Abstractqueuedsynchronizer will make all the request threads a CLH queue, and when a thread executes (Lock.unlock ()) it activates its successor, but the executing thread is not in the queue. While the threads waiting to be executed are all in a blocking state, the explicit blocking of the investigated thread is done by calling Locksupport.park (), and Locksupport.park () calls the Sun.misc.Unsafe.park () local method, further Hotspot in Linux calls the Pthread_mutex_lock function to block the thread from the system kernel.
The queue

As with synchronized, this is also a virtual queue, there are no queue instances, and there is only a pre-and post-relationship between nodes. What is puzzling is why the CLH queue is used? The native CLH queue is used for spin locks, but Doug Lea transformed it into a blocking lock.

When a thread competes for a lock, the line routines first attempts to acquire the lock, which is unfair to those threads already queued in the queue, which is also the origin of the unfair lock, similar to the synchronized implementation, which greatly increases throughput.

If a running thread already exists, the new competitor thread is appended to the end of the queue, specifically with the CAS-based Lock-free algorithm, because a thread concurrently calling CAs on tail may cause other thread CAs to fail, and the workaround is to loop the CAS until successful. Abstractqueuedsynchronizer's implementation is very delicate, breathtaking, not in the details of the full understanding of its essence, the following details the implementation process:

2.1 Sync.nonfairtryacquire

The Nonfairtryacquire method will be the first method that is called indirectly by the lock method, which is called the first time a lock is requested.

/** * Performs non-fair tryLock.  tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */final boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) {        if (compareAndSetState(0, acquires)) {            setExclusiveOwnerThread(current);            return true;        }    }    else if (current == getExclusiveOwnerThread()) {        int nextc = c + acquires;        if (nextc < 0) // overflow            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    return false;}

The method will first determine the current state, if c==0 indicates that no line is impersonating is competing for the lock, if not C!=0 indicates that the thread is owning the lock.

If c==0 is found, the initial call value of Acquires,acquires is set by CAs to 1, each time the thread is re-entered the lock will be +1, each unlock will be 1, but the lock is released for 0 o'clock. If the CAS is set up successfully, you can expect that any other thread to call the CAS will no longer succeed, and that the current thread is getting the lock, as well as the running thread, and it is clear that the running thread has not entered the wait queue.

If C!=0 but discovers that he already owns the lock, simply ++acquires, and modifies the status value, but because there is no competition, so through setstatus modification, rather than CAs, which means that this code implements the function of locking, and the implementation is very beautiful.

2.2 Abstractqueuedsynchronizer.addwaiter

The Addwaiter method is responsible for wrapping a thread that is currently unable to acquire a lock as a node added to the end of the queue:

/** * Creates and enqueues node for current thread and given mode. * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */private Node addWaiter(Node mode) {    Node node = new Node(Thread.currentThread(), mode);    // Try the fast path of enq; backup to full enq on failure    Node pred = tail;    if (pred != null) {        node.prev = pred;        if (compareAndSetTail(pred, node)) {            pred.next = node;            return node;        }    }    enq(node);    return node;}

Where the parameter mode is an exclusive or shared lock, the default is null, which is an exclusive lock. The action added to the end of the team is divided into two steps:

If the current end of the queue already exists (Tail!=null), use CAs to update the current thread to tail
If the current tail is null or the thread calls CAs to set the tail of the queue to fail, the Enq method continues the setup tail
Here is the Enq method:

/** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert * @return node‘s predecessor */private Node enq(final Node node) {    for (;;) {        Node t = tail;        if (t == null) { // Must initialize            if (compareAndSetHead(new Node()))                tail = head;        } else {            node.prev = t;            if (compareAndSetTail(t, node)) {                t.next = node;                return t;            }        }    }}

The method is to cycle through the CAS, even if there is a high concurrency scenario, the infinite loop will eventually successfully append the current thread to the end of the queue (or set the team header). All in all, the goal of Addwaiter is to append the current to the end of the queue through CAs and return to the wrapped node instance.

The main reason to wrap threads as node objects, in addition to using node constructs for virtual queues, is to wrap the various thread states with node, which are carefully designed to be numeric values:
SIGNAL (-1): Thread's successor is impersonating/has been blocked when the thread is release or Cancel to re-thread this successor (Unpark)
CANCELLED (1): The thread has been canceled because of a timeout or interruption
CONDITION (-2): Indicates that the thread is in the conditional queue and is blocked because the condition.await is called
PROPAGATE (-3): Propagating shared locks
0:0 stands for no status

2.3 abstractqueuedsynchronizer.acquirequeued

The primary role of the

Acquirequeued is to block the thread node that has been appended to the queue (the Addwaiter method return value), but before blocking again through Tryaccquire retry whether the lock can be obtained, if the retry succeeds can not block, directly return.

 /** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */final Boo    Lean acquirequeued (final node node, int arg) {Boolean failed = true;        try {Boolean interrupted = false; for (;;)            {final Node P = node.predecessor ();                if (p = = head && tryacquire (ARG)) {Sethead (node); P.next = null;                Help GC failed = false;            return interrupted; } if (Shouldparkafterfailedacquire (p, node) && parkandcheckinterrupt ()) I        Nterrupted = true;    }} finally {if (failed) cancelacquire (node); }}

Take a closer look at this method is an infinite loop, feel if p = = head && tryacquire (ARG) condition does not meet the loop will never end, of course, there will not be a dead loop, The mystery is that the Parkandcheckinterrupt on line 12th will suspend the current thread, blocking the thread's call stack.

/** * Convenience method to park and then check if interrupted * * @return {@code true} if interrupted */private final boolean parkAndCheckInterrupt() {    LockSupport.park(this);    return Thread.interrupted();}

As mentioned earlier, Locksupport.park eventually blocks the thread to the system (Linux) kernel. Of course, it is not immediately blocking the thread that requested the lock, but also checking the state of the thread, such as if the thread is in the cancel State and is not necessary, check in Shouldparkafterfailedacquire:

/** * Checks and updates status for a node, failed to acquire. * Returns True if thread should block.  This is the main signal * control in all acquire loops. Requires that pred = = Node.prev. * * @param pred node ' s predecessor holding status * @param node the node * @return {@code true} if thread should block */p    Rivate Static Boolean Shouldparkafterfailedacquire (node pred, node node) {int ws = Pred.waitstatus;  if (ws = = node.signal)/* * This Node have already set status asking a release * to SIGNAL it, so it         Can safely park.    */return true; if (ws > 0) {/* * predecessor was cancelled.         Skip over predecessors and * indicate retry.        */do {Node.prev = pred = Pred.prev;        } while (Pred.waitstatus > 0);    Pred.next = node;  } else {/* * Waitstatus must be 0 or PROPAGATE.  Indicate that we * need a signal, but don ' t park yet.    Caller'll need to     * Retry to make sure it cannot acquire before parking.    */Compareandsetwaitstatus (pred, WS, node.signal); } return false;}

The principle of inspection is:

    • Rule 1: If the node status of the predecessor is signal, indicating that the current node needs Unpark, the return succeeds, and the 12th line of the Acquirequeued method (Parkandcheckinterrupt) will cause the thread to block
    • Rule 2: If the pre-node state is cancelled (WS>0), indicating that the predecessor node has been discarded, it goes back to a non-canceled forward node, and the infinite loop that returns the False,acquirequeued method recursively calls the method until rule 1 returns true. Cause thread Blocking
    • Rule 3: If the former node state is non-signal, non-cancelled, then the status of the pre-set is signal, return false and enter the infinite loop of acquirequeued, with rule 2

In general, Shouldparkafterfailedacquire is the forward node to determine whether the current thread should be blocked, if the former node is in the cancelled state, then delete these nodes to reconstruct the queue.

Now that the logic to lock the thread is complete, the process of unlocking is discussed below.

3. Unlocking

The thread that requested the lock to fail is suspended on line 12th of the Acquirequeued method, and after 12 lines the code must wait for the thread to be unlocked to execute, and if the blocked thread gets unlocked, then the 13th line is set interrupted = True and then into an infinite loop.

From the infinite Loop code, it can be seen that not the unlocked thread must be able to obtain the lock, the 6th line should be called Tryaccquire re-competition, because the lock is not fair, it is possible to get the newly added thread, causing the newly awakened thread to be blocked again, this detail is fully reflected in the "non-fair" The essence. The unlocking mechanism to be introduced later will see that the first unlocked thread is head, so the judgment of P = = Head is basically successful.

As you can see, the way to defer the Tryacquire method to subclasses is very subtle and highly scalable, and breathtaking! Of course, this is not the templae design pattern, but Doug Lea's careful layout of the lock structure.

The unlocking code is relatively straightforward and is mainly embodied in the Abstractqueuedsynchronizer.release and Sync.tryrelease methods:

Class Abstractqueuedsynchronizer:

/** * Releases in exclusive mode.  Implemented by unblocking one or * more threads if {@link #tryRelease} returns true. * This method can be used to implement method {@link Lock#unlock}. * * @param arg the release argument.  This value is conveyed to *        {@link #tryRelease} but is otherwise uninterpreted and *        can represent anything you like. * @return the value returned from {@link #tryRelease} */public final boolean release(int arg) {    if (tryRelease(arg)) {        Node h = head;        if (h != null && h.waitStatus != 0)            unparkSuccessor(h);        return true;    }    return false;}

Class Sync:

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);    return free;}

Tryrelease is the same as the Tryacquire semantics, delaying the logic of how it is freed to subclasses. The tryrelease semantics are clear: if the thread locks multiple times, it is released several times until status==0 actually releases the lock, and the so-called release lock is set to status 0 because there is no competition so CAS is not used.

The semantics of release is that if a lock can be freed, the first thread (Head) of the queue is awakened, and the specific wake-up code is as follows:

 /** * wakes up node ' s successor, if one exists. * * @param node the node */private void UNPARKSUCC  Essor (node node) {/* * If status is negative (i.e., possibly needing signal) try-to-clear in anticipation of  Signalling.     It is OK if the this * fails or if status are changed by waiting thread.    */int ws = Node.waitstatus;    if (WS < 0) compareandsetwaitstatus (node, WS, 0);  /* * Thread to Unpark are held in successor, which is normally * just the next node.     But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor.    */Node s = node.next;        if (s = = NULL | | s.waitstatus > 0) {s = null;     for (node t = tail; t! = null && t! = Node; t = t.prev) if (t.waitstatus <= 0) s = t; } if (s! = null) Locksupport.unpark (s.thread);}  

This code is meant to find the first thread that can be unpark, generally head.next = = Head,head is the first thread, but Head.next may be canceled or set to NULL, so the more secure way is to find the first available thread from the back. Seemingly backtracking can lead to performance degradation, but the probability of this happening is very small, so there is no performance impact. This is followed by notifying the system kernel to continue the thread, which is done through Pthread_mutex_unlock under Linux. After that, the unlocked thread enters the above-mentioned re-competition state.

4. Lock vs. Synchronize

Abstractqueuedsynchronizer accommodates all blocking threads by constructing a blocking-based CLH queue, and operations on that queue are through Lock-free (CAS) operations, but for the thread that has acquired the lock, The Reentrantlock implements the function of biased locking.

The bottom of the synchronized is also a waiting queue based on CAS operations, but the JVM implements finer, dividing the waiting queue into contentionlist and entrylist, in order to reduce the thread's dequeue speed and, of course, to achieve a biased lock. There is no essential difference between the design and the data structure. However, synchronized also implements spin locks, which are optimized for different systems and hardware architectures, while lock relies entirely on system blocking to suspend waiting threads.

Of course, lock is more suitable for application layer extension than synchronized, can inherit Abstractqueuedsynchronizer define various implementations, such as implement read-write lock (Readwritelock), fair or unfair lock; Lock corresponding to the condition is also more convenient than the wait/notify, more flexible.

Reference:
6641477

The implementation principle of Java lock

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: info-contact@alibabacloud.com 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.