preface
The first part analyzes the implementation details and some difficult points of AQS sharing / exclusive lock. This part continues to analyze the remaining knowledge of AQS. Through this article, you will learn:
1. Exclusive / non interruptible lock
2. Interruptible / non interruptible shared lock
3. Lock that can / cannot wait for a limited time
4. Implementation of waiting / notification
5. Similarities and differences between synchronization queue and waiting queue
6. The difference between Condition.await/Condition.signal and Object.wait/Object.notify
1. Exclusive / non interruptible lock
Definition of interruptible lock
A little analogy:
- Let's start with a scene: Xiao Ming is an Internet addict. He plays games in the Internet cafe all day. His mother calls him to go home for dinner (gives him an interruption order). Xiao Ming verbally promised his mother, but he plays games again as soon as he hangs up. Mother calls repeatedly, but she can't interrupt Xiao Ming's online process. We say that Xiao Ming's online process can't be interrupted.
- Another scene: Although Xiao Ming is an Internet addict, he listens to his mother very much. His mother calls him to go home for dinner (sends him an interruption order). Xiao Ming orally agrees and shuts down, which shows that Xiao Ming's online process can be interrupted.
From the perspective of code, let's look at a section of pseudo code for locking / unlocking:
private void testLock() { myLock.lock(); doSometing1(); doSometing2(); myLock.unlock; } private void doSometing1() {//... } private void doSometing2() {//... }
Lock is an exclusive lock. Threads A and B compete for the lock at the same time. Assuming that A successfully obtains the lock, it can execute doSometing1() and doSometing2() methods.
Thread B calls myLock.lock() because it can't get the lock. However, another thread C is waiting for thread B's data. Because thread B can't get the lock all the time, it can't produce the data required by C. Therefore, C wants to interrupt B. after B is awakened, it has two options:
1. Do not change the original intention, continue to try to obtain the lock, and continue to block when it is not obtained. No matter how C interrupts, it is indifferent.
2. Detect whether the interrupt has occurred. If so, throw an exception directly (I quit and don't try to obtain the lock).
If the design of myLock meets option 1, it is called that the lock cannot be interrupted. If it meets option 2, it is called that the lock can be interrupted.
It is shown as follows:
Non interruptible exclusive lock
Call process of exclusive lock analyzed previously:
acquire(xx)–>acquireQueued(xx)
In acquirequeueueueueued (XX):
As shown in the above figure, only interrupts are detected in places 1 and 2, and then the interrupt value is returned to the caller of the previous layer.
In acquire(xx) on the upper layer:
If you find that an interrupt has occurred, you just fill in the interrupt flag again.
It can be seen that the thread calls acquire(xx) to obtain the lock. If an interrupt occurs, the thread still tries to obtain the lock on its own. External interrupt calls simply cannot terminate its process of obtaining the lock.
At this time, the lock is an exclusive non interruptible lock.
Interruptible exclusive lock
To interrupt the process of obtaining locks, you need to detect the interrupt flag bit, throw an exception or exit the process. See how AQS is implemented:
#AbstractQueuedSynchronizer.java public final void acquireInterruptibly(int arg) throws InterruptedException { //If an interrupt occurs, an interrupt exception is thrown if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; //Because the interrupt has been detected and thrown below, there is no need to return the interrupt status here return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //When you wake up, you find that you have been interrupted, so you throw an interrupt exception directly throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
It can be seen that exceptions are thrown in two places:
1. Check whether the interrupt has occurred before preparing to acquire the lock. If so, throw an exception.
2. After being awakened, check whether the interrupt has occurred. If so, throw an exception and interrupt the process of obtaining the lock.
2. Interruptible / non interruptible shared lock
Non interruptible shared lock
The calling process of shared lock analyzed previously:
acquireShared(xx)–>doAcquireShared(xx)
In doAcquireShared(xx):
Unlike the non interruptible exclusive lock, doAcquireShared(xx) directly fills the interrupt flag bit after detecting the interrupt and does not need to be passed to the outer layer.
Of course, like non interruptible exclusive locks, they do not handle interrupts.
At this time, the lock is a non interruptible shared lock.
Interruptible shared lock
#AbstractQueuedSynchronizer.java public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) //If an interrupt occurs, an interrupt exception is thrown throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; //Because the interrupt has been detected and thrown below, there is no need to supplement the interrupt here return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //When you wake up, you find that you have been interrupted, so you throw an interrupt exception directly throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
As you can see, it is consistent with the logic of interruptible exclusive lock processing interrupt.
3. Lock with / without time limit
Exclusive lock with time limited wait
It can be seen from the above that although the lock can respond to an interrupt, there are other scenarios that are not covered:
The thread does not want to wait all the time to acquire the lock, but wants to wait for a certain time. If the lock is not acquired, it will give up acquiring the lock.
Let's take a look at the time limited waiting lock mechanism provided in AQS:
#AbstractQueuedSynchronizer.java public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { //Pass in the nanosTimeout parameter. The unit is nanosecond. It means that if the time is exhausted or the lock is not obtained, the lock acquisition process will exit if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { //Time required > 0 if (nanosTimeout <= 0L) return false; //Calculation deadline final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } //Check to see if time is running out nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) //Run out, then exit directly return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) //Wake up after a certain period of sleep LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
It can be seen that after a thread is suspended, there are two ways to wake it up:
1. External call interrupt method.
2. The hang time limit has arrived.
When the thread is awakened, check the interrupt flag bit. If an interrupt occurs, throw an exception directly. Otherwise, try to obtain the lock again. If obtaining the lock fails, judge whether it has timed out. If so, exit.
Shared lock that can wait for a limited time
Similar to an exclusive lock that can wait for a limited time, it will not be described in detail.
4. Implementation of waiting / notification
Basic data structure
AbstractQueuedSynchronizer has a subclass: ConditionObject
It is the basis of waiting / notification.
Let's take a look at its structure:
#ConditionObject //Point to the head of the team private transient Node firstWaiter; //Point to the end of the team private transient Node lastWaiter;
firstWaiter and lastWaiter jointly maintain the waiting queue in combination with the nextWaiter pointer in the Node:
If the data structure is available, you need the method of operating the data structure:
It can be seen that there are two methods: awaitXX(xx)/signalXX():
await(): wait indefinitely.
await(long time, TimeUnit unit): wait for a limited time. You can specify the time unit.
awaitNanos(long nanosTimeout): wait for a limited time. The time unit is nanoseconds.
Awaituninterruptible(): non interruptible time limited wait.
awaitUntil(Date deadline): wait for a limited time. Specify the timeout time as a point in the future.
signal(): notify the nodes in the waiting queue.
signalAll(): notifies all nodes in the waiting queue.
Thread A calls awaitXX(xx) to block and wait for the condition to be met, and thread B calls signalXX() to notify thread A that the condition is met. Obviously, this is A thread synchronization process.
Take await()/signal()/signalAll() methods to analyze the waiting / notification mechanism.
ConditionObject.await() implementation
public final void await() throws InterruptedException { //An interrupt occurs and an exception is thrown if (Thread.interrupted()) throw new InterruptedException(); //Join the waiting queue Node node = addConditionWaiter();//--------->(1) //Release lock all int savedState = fullyRelease(node);//--------->(2) int interruptMode = 0; //If not in the synchronization queue while (!isOnSyncQueue(node)) {//--------->(3) //Suspend thread LockSupport.park(this); //After the thread is awakened, check whether an interrupt has occurred ----------- > (4) //There are two reasons for being awakened: 1. An interrupt occurs, and 2. Another thread calls signal if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //Get synchronization status if (acquireQueued(node, savedState) && interruptMode != THROW_IE) //If successful, the marking thread needs to reset the interrupt flag bit interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled //Remove the cancelled node from the waiting queue --------- > (5) unlinkCancelledWaiters(); //Decide how to handle interrupts if (interruptMode != 0) reportInterruptAfterWait(interruptMode);//--------->(6) }
Throughout the await() method, three main tasks have been done:
1. Encapsulate the thread to the Node node and join the waiting queue.
2. The pending thread waits for the condition to be met.
3. After the thread wakes up, it scrambles for the lock.
Six key methods are marked in the note. Let's take a look at them respectively:
(1)
private Node addConditionWaiter() { //Find the tail node Node t = lastWaiter; if (t != null && t.waitStatus != Node.CONDITION) { //If it is found that the waiting state is not a CONDITION, the waiting queue is traversed to remove the cancelled node unlinkCancelledWaiters(); t = lastWaiter; } //Construction node Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) //If the queue is empty, the header pointer points to the current node firstWaiter = node; else //After hanging the current node to the tail node t.nextWaiter = node; //The tail pointer points to the tail node lastWaiter = node; return node; }
Function of this method: encapsulate the thread as a node and add it to the waiting queue.
(2)
Since you have to wait, release the lock so that other threads can obtain the lock and do the corresponding operation.
final int fullyRelease(Node node) { boolean failed = true; try { //Gets the current synchronization status int savedState = getState(); //Release synchronization status all if (release(savedState)) { //Release successful failed = false; return savedState; } else { //Failure to release indicates that the current lock is not exclusive, and an exception is thrown throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; } }
From this method, we can see that when await() is called, the AQS must be guaranteed to be an exclusive lock.
(3)
final boolean isOnSyncQueue(Node node) { //If the current node status is CONDITION or the node precursor node is null, it means that the node is not in the synchronization queue if (node.waitStatus == Node.CONDITION || node.prev == null) return false; //If the node state is not CONDITION and the node precursor node exists and the successor node exists, it is considered to be in the synchronization queue if (node.next != null) return true; //The successor node does not exist. It may be that when the node is added to the tail of the synchronization queue, CAS failed to modify the tail. Therefore, traverse the synchronization queue here and directly compare whether the nodes are equal return findNodeFromTail(node); }
You need to ensure that it is not in the synchronization queue to operate.
(4)
private int checkInterruptWhileWaiting(Node node) { //If an interrupt occurs, transfer after canceledwait will be called to judge. Otherwise, 0 will be returned directly return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; } final boolean transferAfterCancelledWait(Node node) { //This indicates that the thread has been interrupted. The next step is to judge whether the signal action has occurred //Attempt to modify node state if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { //If successful, it will be added to the synchronization queue //CAS success indicates that signal() has not occurred at this time enq(node); return true; } //Explain that CAS failed because the node state has been changed in signal(). //Therefore, you only need to wait for signal() to add the node to the synchronization queue. while (!isOnSyncQueue(node)) Thread.yield(); return false; }
Transferaftercanceledwait (XX) returns true, indicating that an interrupt exception needs to be thrown, and returns false, indicating that you only need to fill in the interrupt flag bit.
interruptMode!=0, indicating that an interrupt has occurred and directly exit the cycle.
(5)
Although the node has been added to the synchronization queue, it may not be removed from the waiting queue (without calling signal), so it needs to be checked here.
(6)
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { //An exception is thrown directly. An interrupt occurs, but there is no signal if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) //Fill in the interrupt flag bit selfInterrupt(); }
It can be seen that even if an interrupt occurs, await(xx) will not process the interrupt until it obtains the lock, which ensures that the thread must obtain the lock when returning from the await(xx) call.
ConditionObject.signal() implementation
public final void signal() { if (!isHeldExclusively()) //If it is not an exclusive lock, an exception is thrown throw new IllegalMonitorStateException(); //First node found Node first = firstWaiter; if (first != null) doSignal(first); } private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) //If there is no subsequent node, the tail pointer is null lastWaiter = null; //Remove the header node from the wait queue first.nextWaiter = null; } while (!transferForSignal(first) &&//Move node to synchronization queue //The header pointer points to the next node (first = firstWaiter) != null); } final boolean transferForSignal(Node node) { //modify state if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //Join synchronization queue Node p = enq(node); int ws = p.waitStatus; //p is the precursor node of the node. If the precursor node is cancelled or the state modification fails, it will directly wake up the thread associated with the current node if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
It can be seen that when calling signal(), you need to ensure that the AQS is an exclusive lock and that the current thread has obtained the exclusive lock.
Throughout the whole signal() method, there are three main points:
1. Remove the node from the waiting queue.
2. Join the node to the synchronization queue.
3. If there is only one node in the synchronization queue or the modification of the state of the precursor node fails, wake up the current node.
ConditionObject.signalAll() implementation
public final void signalAll() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignalAll(first); } private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; //Remove from waiting queue first.nextWaiter = null; //Join to synchronization queue transferForSignal(first); first = next; } while (first != null); }
It can be seen that signal() only moves the head node of the waiting queue to the synchronization queue, while signalAll() moves all nodes in the waiting queue to the synchronization queue.
5. Similarities and differences between synchronization queue and waiting queue
1. The synchronization queue is implemented by a two-way linked list (FIFO). The precursor and successor nodes are linked through the Node.prev/Node.next pointer.
2. The waiting queue is implemented by a one-way linked list (FIFO), and the subsequent nodes are linked through the Node. nextWaiter pointer.
3. The nodes in both queues are of Node type.
4. If the lock acquisition fails, it will be added to the tail of the synchronization queue for waiting. If the lock acquisition succeeds, it will be removed from the synchronization queue.
5. Calling await() will be added to the tail of the waiting queue, and calling signal() will be removed from the head of the waiting queue and added to the tail of the synchronization queue.
6. The difference between Condition.await/Condition.signal and Object.wait/Object.notify
Let's start with a code:
Wait / notify application of Object.java:
Object object = new Object(); private void testObjectWait() { synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } private void testObjectNotify() { synchronized (object) { object.notify(); } }
Let's look at the Condition waiting / notification application:
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); Condition condition2 = lock.newCondition(); private void testConditionAwait() { lock.lock(); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } private void testConditionSignal() { lock.lock(); try { condition.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }
Similarities and differences between the two:
Same point
1. The corresponding method can only be called after obtaining the lock. Obtaining the lock is to ensure the correctness of the condition variable under the condition of thread concurrency.
2. Wait / notify methods appear in pairs.
3. Waiting methods can respond to interrupts.
4. All wait methods support timeout return.
difference
Condition waiting / notification depends on AQS, that is, it needs to be used with Lock and implemented in JDK.
Object wait / notification depends on the synchronized keyword and is implemented in the JVM.
In addition, the Condition wait / notification mechanism is more flexible than the Object wait / notification mechanism, as follows:
1. Condition wait can respond to an interrupt or not.
2. Condition wait can set the timeout to a future point in time.
2. The same lock can generate multiple conditions.
The next article will focus on the implementation and application of subclass wrappers derived from AQS, such as ReentrantLock, ReentrantReadWriteLock, Semaphore and CountDownLatch.
This paper is based on jdk1.8.