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.
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 exclusiveOwnerThread
property, 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 doAcquireShared
internal method, which we analyzed in the following doAcquireShared
time 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.
acquireShared
The tryAcquireShared
method responsible for specific sub-class implementation, where if we do not watch.
Next we look at doAcquireShared
the 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 acquireQueued
method:
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
So, together, both logically only two differences:
addWaiter(Node.EXCLUSIVE)
->addWaiter(Node.SHARED)
setHead(node)
->setHeadAndPropagate(node, r)
The first point here is different exclusive lock acquireQueued
call 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 nextWaiter
attributes:
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
We know that, in the condition queue nextWaiter
points to the next queue condition of a node, the node will queue condition string together to form a single linked list. But in sync queue
the queue, we only use prev
, next
attributes to nodes in series to form a doubly linked list, nextWaiter
properties 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 doReleaseShared
method, 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 setHeadAndPropagate
likely 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 releaseShared
when 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 acquireShared
the end of the process, when the thread successfully acquires a shared lock, call the method under certain conditions; one in the releaseShared
process, 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 releaseShared
a 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 releaseShared
invoked 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 acquireShared
call, or the releaseShared
call 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, unparkSuccessor
basically 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 setHeadAndPropagate
call a method in doReleaseShared
wake 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 continue
in 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:
- First of all, the premise is a queue of at least two nodes
- Secondly, we must execute the
else if
statement 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 performshouldParkAfterFailedAcquire
, it predecessor node (i.e. the first node) waitStatus modify the valueNode.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 &&
- Then, to satisfy
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
this condition, indicating that at this time the head nodewaitStatus
is 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 if
the &&
connected state of the two inconsistent respectively correspond shouldParkAfterFailedAcquire
to compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
the front and rear successful execution executed successfully, as doReleaseShared
, and
shouldParkAfterFailedAcquire
can 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 doReleaseShared
call to unparkSuccessor
a method unpark
of and not a park
thread. However, this operation is permitted, when we unpark
a have not been park
the thread when the thread calls the next time park
when 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)