[Reserved] source code line by line analysis of AQS (3) - Share lock acquisition and release

Address reprint:

AQS source code line by line analysis (3) - Share lock acquisition and release

Foreword

We have two front ReentrantLock an example to understand the AQS exclusive lock acquisition and release , this article, we take a look at shared lock. Since AQS more similar framework for the realization of shared locks and exclusive locks, so if you get in front of an exclusive lock mode, the lock will share a very easy to understand.

Series of articles directory

The difference between shared and exclusive lock lock

Shared locks and exclusive locks biggest difference is that an exclusive lock is exclusive, exclusive , and therefore there is a lock in exclusive exclusiveOwnerThreadproperty, used to record the thread currently holds the lock. When an exclusive lock has been held by a thread, other threads can only wait for it to be released, in order to fight the lock, and that only one thread can successfully lock contention.

For shared lock, since the lock can be shared, so that it can be held by multiple threads simultaneously . In other words, if a thread successfully acquires a shared lock, then wait for the other thread to this shared lock can also try to acquire the lock, and most likely to succeed.

The realization of shared locks and exclusive locks are corresponding, we can see from this table below:

Exclusive lock Shared lock
tryAcquire(int arg) tryAcquireShared(int arg)
tryAcquireNanos(int arg, long nanosTimeout) tryAcquireSharedNanos(int arg, long nanosTimeout)
acquire(int arg) acquireShared(int arg)
acquireQueued(final Node node, int arg) doAcquireShared(int arg)
acquireInterruptibly(int arg) acquireSharedInterruptibly(int arg)
doAcquireInterruptibly(int arg) doAcquireSharedInterruptibly(int arg)
doAcquireNanos(int arg, long nanosTimeout) doAcquireSharedNanos(int arg, long nanosTimeout)
release(int arg) releaseShared(int arg)
tryRelease(int arg) tryReleaseShared(int arg)
- doReleaseShared()

As can be seen, except for the last part of a shared lock doReleaseShared()method does not correspond, the other methods, a shared lock and an exclusive lock is one to one.

In fact, fact, and doReleaseShared()corresponding method should be the exclusive lock unparkSuccessor(h), but doReleaseShared()logic is not only included unparkSuccessor(h), but also includes other operations, which we analyze the following source we'll see.

In addition, in particular, it should be noted that, in an exclusive lock mode, we can only get the exclusive lock lock release node, the node will wake successor - This is reasonable, because an exclusive lock can only be held by a thread, If it has not been released, there is no need to awaken its successor node.

However, in a shared lock mode, when a node to get a shared lock, we can wake up in the successor node after the acquisition is successful, without the need to wait until the lock is released when the node, because the lock can be shared simultaneously by multiple threads hold, to get a lock, and the subsequent nodes can be obtained directly. Therefore, in a shared lock mode, at the end of the acquisition and release locks, will awaken successor node. This is also the doReleaseShared()method and unparkSuccessor(h)the root cause method does not directly correspond to the location.

Shared lock acquisition

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

We take it and compare exclusive lock modes:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

Structure both seem a little difference, but the fact is the same, just under a shared lock mode, and addWaiter(Node.EXCLUSIVE)the corresponding addWaiter(Node.SHARED), and selfInterrupt()all operations moved to the doAcquireSharedinternal method, which we analyzed in the following doAcquireSharedtime method to clear out .

But here, a first plug, an exclusive lock with respect to the tryAcquire(int arg)return of a boolean value, a shared lock tryAcquireShared(int acquires)returns an integer value:

  • If the value is less than zero, it represents the current thread acquires a shared lock failure
  • If the value is greater than zero, it represents the current thread acquires a shared lock success, and subsequently other thread tries to acquire a shared lock behavior is likely to succeed
  • If the value is equal to 0, representing the current thread acquires a shared lock is successful, but then the other thread tries to acquire a shared lock behavior fail

Therefore, if the return value is greater than or equal to 0, it indicates successful acquire a shared lock.

acquireSharedThe tryAcquireSharedmethod responsible for specific sub-class implementation, where if we do not watch.

Next we look at doAcquireSharedthe method, which corresponds to an exclusive lock acquireQueued, there is actually very similar, we put them in the same section commented out, just look at different parts:

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    /*boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();*/
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            /*if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }*/
}

If part on the above, an exclusive lock corresponding acquireQueuedmethod:

if (p == head && tryAcquire(arg)) {
    setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
}

So, together, both logically only two differences:

  1. addWaiter(Node.EXCLUSIVE) -> addWaiter(Node.SHARED)
  2. setHead(node) -> setHeadAndPropagate(node, r)

The first point here is different exclusive lock acquireQueuedcall is addWaiter(Node.EXCLUSIVE), and the call is shared lock addWaiter(Node.SHARED), indicating that the node is shared mode, the definition of these two models are:

/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;

This mode is assigned to the node nextWaiterattributes:

Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

We know that, in the condition queue nextWaiterpoints to the next queue condition of a node, the node will queue condition string together to form a single linked list. But in sync queuethe queue, we only use prev, nextattributes to nodes in series to form a doubly linked list, nextWaiterproperties here only plays a marked role , not tandem node, here not to be Node SHARED = new Node()pointed to the empty node confused, empty node does not belong sync queue, does not represent any thread, it only functions as a marker, determining whether the node is only used as a basis in the shared mode:

// Node#isShard()
final boolean isShared() {
    return nextWaiter == SHARED;
}

The second difference here is that the behavior of the lock acquisition success, for the exclusive lock, it is a direct call to a setHead(node)method, and a shared lock call is setHeadAndPropagate(node, r):

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);

    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

We not only inside the method call setHead(node), the call is still under certain conditions, doReleaseShared()to wake up the subsequent node. This is because in a shared lock mode, the lock can be held together multiple threads, since the current thread has got a shared lock, then you can come and collect inform immediate successor node lock, without waiting for the time lock to be released again Notice.

About this doReleaseSharedmethod, we analyzed the time to look at the lock release below.

Shared lock release

We use the releaseShared(int arg)method to release the shared locks:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

This method corresponds to the exclusive lock release(int arg)method:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

In an exclusive lock mode, since the head node is the node holds an exclusive lock, exclusive lock after it is released, if you find yourself in waitStatus not 0, it will wake responsible for its successor node.

In a shared lock mode, the head node is the node holding shared locks, shared locks after it is released, it should also awaken its successor nodes, but it is worth noting that before we are setHeadAndPropagatelikely to have been called the method method , and that is it may be the same head node invoked twice , may also be in us from releaseSharedwhen to call it method, the current head node has changed hands, let's take a closer look at this method:

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

This method may be a method of sharing the lock mode is the hardest to understand, when looking at the process, we need to clear the following questions:

(1) This method has several call?

This method has two calls, one in acquireSharedthe end of the process, when the thread successfully acquires a shared lock, call the method under certain conditions; one in the releaseSharedprocess, when the thread releases the lock of the shared call.

Who (2) threads are calling the method?

In an exclusive lock, only the thread that acquired the lock to release the lock release calls, so calling unparkSuccessor (h) must be the wake-up successor node holding the lock thread that can be seen as the current head node (although setHead method the thread has been the head node attribute set to a null, but this is the first node have represented this thread)

In a shared lock, the thread holds a shared lock can have more, these threads can call releaseShareda method to release the lock; and these threads want to get a shared lock, then they must have become too far node, or is now the head node . Therefore, if it is releaseSharedinvoked method doReleaseShared, the method may call at this time is not the thread head node represents a thread, and the head node may have been changed hands several times.

What is the purpose (3) of this method is invoked?

Whether the acquireSharedcall, or the releaseSharedcall process, the object of this method is currently available is shared lock state, the wake-up head node of a node . It looks and exclusive locks seem the same, but they are an important difference is that - in a shared lock, the head node changes, will return to the cycle and then immediately wake up the next node of the head node. In other words, when after the current node to complete the task wake successor node will quit if found to be awakened successor node has become the new head node will immediately trigger the next node wake-head node operation, again and again.

(4) What are the conditions to exit the process

This method is a spin operation ( for(;;)), the only way to exit the method is to take the last break statement:

if (h == head)   // loop if head changed
    break;

That is, only when the current head is not easy to master, will withdraw from, or continue the cycle.
How this understanding it?
To illustrate the problem, here we assume that the current sync queue in the queue are arranged in order

dummy node -> A -> B -> C -> D

Now suppose that A has got a shared lock, it will become the new dummy node,

dummy node (A) -> B -> C -> D

At this point, A thread calls doReleaseShared, we do write doReleaseShared[A], wake up in the subsequent node B of the method, it quickly gained a shared lock to become the new head node:

dummy node (B) -> C -> D

At this point, B thread will call doReleaseShared, we do write doReleaseShared[B], Wake successor of the node C in the method, but do not forget, at the doReleaseShared[B]time of the call, doReleaseShared[A]not run over yet, when it runs into if(h == head), we found the head node has now been changed, so it will continue to return for the cycle, while doReleaseShared[B]also pitching in, it is in the implementation process has also entered into a for loop. . .

Thus, here we formed a doReleaseShared of " calling the storm ", a large number of threads executing doReleaseShared the same time, which greatly accelerated the speed wake successor node, improved efficiency, while inside the CAS method of operation and ensures more than when a node wake threads simultaneously, only one thread can operate successfully.

What if this doReleaseShared[A]time upon completion, node B has not yet become the new head node, doReleaseShared[A]the method does not quit yet? Yes, but even that does not matter, because it has successfully awakened thread B, even doReleaseShared[A]quit, and when the thread B to become the new head node, doReleaseShared[B]it started, and it will be responsible for wake successor node, so that even if the change each node only wake up to this mode own successor node, from a functional perspective, the ultimate aim can be achieved wake up all waiting for nodes shared locks, but not before on efficiency, "calling the storm" fast.

From this we know here, "calling the storm" is in fact an optimization operation, because when we are at the end of the method, unparkSuccessorbasically has been called up, but as it is a shared lock mode, so to be awakened successor node most likely to have been acquired shared locks, became the new head node, when it becomes the new head node, it may still want to setHeadAndPropagatecall a method in doReleaseSharedwake of its successor node.

Clear after a few questions above, let us analyze in detail this method, it is the most important part is if the following two statements:

if (ws == Node.SIGNAL) {
    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
        continue;            // loop to recheck cases
    unparkSuccessor(h);
}
else if (ws == 0 &&
         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
    continue;                // loop on failed CAS

If first well understood, if the current value is Node.SIGNAL ws, then the subsequent node needs to wake up, CAS operation used here Node.SIGNAL first state to 0, this is because said before, there may be a large number of doReleaseShared while performing the method, we only need to perform one unparkSuccessor(h)operation on the line, here by CAS to ensure the operation unparkSuccessor(h)is executed only once.

More difficult to understand is the second else if, first of all we have to figure out ws Shashi 0, one is above compareAndSetWaitStatus(h, Node.SIGNAL, 0)will lead ws is 0, it is clear that if it is because of this reason, it is not going to enter into else if statement block. Therefore it ws where 0 means the current queue is the last node to become a head node . Why is the last node of it, because every time a new node add to the mix, some will hang in front of his predecessor node waitStatus modified to Node.SIGNAL of. (See detail on this point do not understand here )

Secondly, compareAndSetWaitStatus(h, 0, Node.PROPAGATE)this operation when it will fail? Since this operation fails, the moment in the implementation of this operation, ws at this time has not 0, indicating there is a new node into the team, and was changed to a value ws Node.SIGNAL, this time we will call continuein the next cycle directly into this new team just ready to hang but the thread wakes up.

In fact, if we combine the overall external conditions, it is easy to understand the scene for this case, do not forget to enter the above paragraph is also a condition

if (h != null && h != tail)

It is in the outermost layer:

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) { // 注意这里说明了队列至少有两个节点
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;               
        }
        if (h == head)
            break;
    }
}

This condition implies that there are at least two nodes in the queue.

Combined with the above analysis, we can see that this

else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

It describes an extremely harsh and transient states:

  1. First of all, the premise is a queue of at least two nodes
  2. Secondly, we must execute the else ifstatement that we skipped the previous if condition, indicating just become the head node is the head node, it is also waitStatus value is 0, the node is the tail after that just add to the mix, it needs to perform shouldParkAfterFailedAcquire, it predecessor node (i.e. the first node) waitStatus modify the value Node.SIGNAL, but now the modification operation has not come and perform . This situation allows us to enter else if the first half ofelse if (ws == 0 &&
  3. Then, to satisfy !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)this condition, indicating that at this time the head node waitStatusis not 0, and this shows that there is no time to be executed before the waitStatus value precursor node changes in operating Node.SIGNAL of shouldParkAfterFailedAcquire now run over.

Thus, else ifthe &&connected state of the two inconsistent respectively correspond shouldParkAfterFailedAcquireto compareAndSetWaitStatus(pred, ws, Node.SIGNAL)the front and rear successful execution executed successfully, as doReleaseShared, and
shouldParkAfterFailedAcquirecan be performed concurrently, it is possible to meet this condition, the condition is only satisfied stringent , it may be just the moment thing.

Here have to say, if the above analysis is not wrong, then it is for the author to optimize AQS performance has reached a "heinous" the point! ! ! Although this brief moment does exist, and indeed necessary for loop back again to wake successor node, but this optimization is also too ~ ~ ~ wife too fine it!

We take a look if you do not join this fine control conditions have consequences?

Here we review the process of the new node into the team, as I said before, the discovery of the precursor of the new node is not a head of time, it calls shouldParkAfterFailedAcquire:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has 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 will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

Since the value of ws predecessor node now is 0, the new node will change it to Node.SIGNAL,

But after the changes, the method returns false, the thread that is not suspended immediately, but returned to try again to grab the upper lock:

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // shouldParkAfterFailedAcquire的返回处
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

When we return to again for(;;)cycle, because this time the precursor of the current node has become the new head, so it can participate grab the lock, grab because it is a shared lock, so it is a big probability to grab, so very it will not likely be suspended. This may lead to the above doReleaseSharedcall to unparkSuccessora method unparkof and not a parkthread. However, this operation is permitted, when we unparka have not been parkthe thread when the thread calls the next time parkwhen the method will not be suspended, and this behavior is in line with our scenario - because the current share lock in the state can be obtained, subsequent thread should acquire the lock directly, it should not be suspended.

In fact, I think:

else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
    continue;  // loop on failed CAS

In fact, this period can be omitted, of course, with this period will certainly speed up the process of wake successor node, the author optimized for extremely short above the kind of situation before and it can be said to "call the storm," the design of the same strain, may also be positive It is due to the pursuit of the ultimate performance for AQS makes it so good.

to sum up

  • The framework calls shared lock and an exclusive lock is very similar, the biggest difference is that they acquire the lock logic - shared lock can be held by multiple threads simultaneously, and the same time an exclusive lock can only be held by a thread.
  • Due to time share the same lock can be held by multiple threads, so the head node to acquire a shared lock, immediately subsequent node can wake up to fight the lock without having to wait until the release of the lock. Thus, a shared lock trigger wake successor node behavior may have two, one in the current node successfully shared lock, in a shared lock is released after the current node.

(Finish)

Series of articles directory

Guess you like

Origin www.cnblogs.com/hongdada/p/12097086.html