In the previous article "Java Concurrency Series [1]-----AbstractQueuedSynchronizer Source Code Analysis", we introduced some basic concepts of AbstractQueuedSynchronizer, mainly talking about how the queue area of AQS is implemented, what is the exclusive mode and the shared mode, and how to understand the waiting state of nodes. Understanding and mastering these contents is the key to subsequent reading of AQS source code, so it is recommended that readers read my previous article first and then look back at this article to understand it. In this article, we will introduce how nodes enter the synchronization queue queue in exclusive mode and what operations will be performed before leaving the synchronization queue. AQS provides three ways to obtain locks in exclusive mode and shared mode: non-responsive thread interrupt acquisition, response thread interrupt acquisition, and set timeout acquisition. The overall steps of these three methods are roughly the same, with only a few different parts, so if you understand one method and look at the implementation of other methods, you will be similar. In this article, I will focus on the acquisition method of not responding to thread interrupts, and the other two methods will also talk about the inconsistencies.
1. How to obtain locks with non-responsive thread interrupts?
//Not respond to interrupt method acquisition (exclusive mode) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); }}Although the above code looks simple, it performs the 4 steps shown in the figure below in order. Below we will demonstrate and analyze step by step.
Step 1:!tryAcquire(arg)
//Try to acquire the lock (exclusive mode) protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException();}At this time, someone came and he tried to knock on the door first. If he found that the door was not locked (tryAcquire(arg)=true), he would go in directly. If you find that the door is locked (tryAcquire(arg)=false), then perform the next step. This tryAcquire method determines when the lock is open and when the lock is closed. This method must be overwritten by subclasses and rewrite the judgment logic inside.
Step 2: addWaiter(Node.EXCLUSIVE)
//Wrap the current thread into a node and add it to the tail of the synchronization queue private Node addWaiter(Node mode) { //Specify the mode holding the lock Node node = new Node(Thread.currentThread(), mode); //Get the reference of the tail node of the synchronization queue Node pred = tail; //If the tail node is not empty, it means that the synchronization queue already has a node if (pred != null) { //1. Point to the current tail node node.prev = pred; //2. Set the current node to the tail node if (compareAndSetTail(pred, node)) { //3. Point the successor of the old tail node to the new tail node pred.next = node; return node; } } // Otherwise, it means that the synchronization queue has not been initialized enq(node); return node;}//Node enqueue private Node enq(final Node node) { for (;;) { //Get the reference of the tail node of the synchronization queue Node t = tail; //If the tail node is empty, it means that the synchronization queue has not been initialized if (t == null) { //Initialize the synchronization queue if (compareAndSetHead(new Node())) { tail = head; } } else { //1. Point to the current tail node node.prev = t; //2. Set the current node to the tail node if (compareAndSetTail(t, node)) { //3. Point the successor of the old tail node to the new tail node t.next = node; return t; } } }}Execution to this step indicates that the first time the lock acquisition failed, so the person will get a number card for himself and enter the queue area to queue. When receiving the number card, he will declare how he wants to occupy the room (exclusive mode or sharing mode). Note that he did not sit down and rest at this time (hang himself up).
Step 3: acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
//Acquiring the lock in an uninterruptible manner (exclusive mode) final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //Get the reference of the previous node of the given node final Node p = node.predecessor(); //If the current node is the first node of the synchronization queue, try to acquire the lock if (p == head && tryAcquire(arg)) { //Set the given node as the head node setHead(node); //To help garbage collection, clear the successor of the previous head node p.next = null; //Set the successful acquisition state failed = false; //Return the interrupted state, the entire loop is executed here, the exit return interrupted; } //Otherwise it means that the lock status is still unavailable. At this time, determine whether the current thread can be suspended//If the result is true, the current thread can be suspended, otherwise the loop will continue, during this period, the thread will not respond to the interrupt if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { interrupted = true; } } } finally { //Make sure to cancel the acquisition if (failed) { cancelAcquire(node); } }}//Judge whether it can suspend the current node private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //Get the waiting state of the forward node int ws = pred.waitStatus; //If the forward node state is SIGNAL, it means that the forward node will wake up the current node, so the current node can safely suspend if (ws == Node.SIGNAL) { return true; } if (ws > 0) { //The following operation is to clean up all cancelled forward nodes in the synchronization queue do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //To this end, it means that the state of the forward node is not SIGNAL, and it is likely to be equal to 0. In this way, the forward node will not wake up the current node.//So the current node must ensure that the state of the forward node is SIGNAL to hang it safely compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}//Suspend the current thread private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted();}After getting the number sign, he will immediately implement this method. When a node enters the queue area for the first time, there are two situations. One is that he finds that the person in front of him has left his seat and entered the room, so he will not sit down and rest, and will knock on the door again to see if the kid is done. If the person inside just happened to be finished, he would rush in without calling himself. Otherwise, he would have to consider sitting down and resting for a while, but he was still worried. What if no one reminded him after he sat down and fell asleep? He left a small note on the seat of the man in front so that the person who came out from inside could wake him up after seeing the note. Another situation is that when he entered the queue area and found that there were several people queuing in front of him, he could sit down for a while, but before that, he would still leave a note on the seat of the person in front (he was already asleep at this time) so that the person could wake him up before leaving. When everything is done, he sleeps peacefully. Note that we see that the entire for loop has only one exit, that is, it can only go out after the thread successfully acquires the lock. Before the lock is obtained, it is always hung in the parkAndCheckInterrupt() method of the for loop. After the thread is awakened, it also continues to execute the for loop from this place.
Step 4: selfInterrupt()
//The current thread will interrupt itself private static void selfInterrupt() { Thread.currentThread().interrupt(); }Since the entire thread above has been hung in the parkAndCheckInterrupt() method of the for loop, it does not respond to any form of thread interrupt before it is successfully acquired. Only when the thread successfully acquires the lock and comes out of the for loop will it check whether someone asks to interrupt the thread during this period. If so, call the selfInterrupt() method to hang itself.
2. How to obtain locks in response to thread interrupts?
//Acquiring the lock in interruptible mode (exclusive mode) private void doAcquireInterruptibly(int arg) throws InterruptedException { //Wrapping the current thread into a node and adding it to the synchronization queue final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { //Getting the previous node final Node p = node.predecessor(); //If p is a head node, then the current thread tries to acquire the lock again if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; //Return return after the lock is successfully acquired; } // If the condition is met, the current thread will be suspended. At this time, an interrupt is responded and an exception is thrown if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { // If the thread is awakened, an exception will be thrown if the interrupt request is found, a failure will be thrown. throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } }}The response thread interrupt method and the non-responsive thread interrupt method are roughly the same in the process of obtaining locks. The only difference is that after the thread wakes up from the parkAndCheckInterrupt method, it will check whether the thread is interrupted. If so, it will throw an InterruptedException exception. Instead of responding to thread interrupt acquisition lock, it only sets the interrupt state after receiving the interrupt request, and will not immediately end the current method of acquiring the lock. It will not decide whether to hang itself based on the interrupt state after the node successfully acquires the lock.
3. How to set the timeout time to acquire the lock?
//Acquiring the lock with a limited timeout (exclusive mode) private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { //Getting the current system time long lastTime = System.nanoTime(); //Wrapping the current thread into a node and adding it to the synchronization queue final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { //Getting the previous node final Node p = node.predecessor(); //If the previous node is a head node, then the current thread tries to acquire the lock again if (p == head && tryAcquire(arg)) { //Update the head node setHead(node); p.next = null; failed = false; return true; } //Once the timeout time is used up, exit the loop directly if (nanosTimeout <= 0) { return false; } //If the timeout time is greater than the spin time, then after judging that the thread can be suspended, the thread will be suspended for a period of time if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) { //Have the current thread for a period of time, and then wake up by itself LockSupport.parkNanos(this, nanosTimeout); } //Get the current time of the system long now = System.nanoTime(); //Timeout time is subtracted from the time interval of the acquisition lock nanosTimeout -= now - lastTime; //Update lastTime again lastTime = now; //Exception is thrown when an interrupt request is received during the acquisition of the lock if (Thread.interrupted()) { throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } }}Setting the timeout time acquisition will first acquire the lock. After the first time acquisition fails, it will be based on the situation. If the incoming timeout time is greater than the spin time, the thread will be suspended for a period of time, otherwise it will be spinning. After each time the lock is acquired, the timeout time will be subtracted from the time taken to acquire the lock. Until the timeout is less than 0, it means that the timeout time has been used up. Then the operation of acquiring the lock will be terminated and the acquisition failure flag will be returned. Note that during the process of acquiring the lock with the timeout time, you can respond to thread interrupt requests.
4. How does the thread releases the lock and leaves the synchronization queue?
//Release lock operation (exclusive mode) public final boolean release(int arg) { //Turn the password lock to see if it can unlock if (tryRelease(arg)) { //Get the head node Node h = head; //If the head node is not empty and the waiting state is not equal to 0, wake up the successor node if (h != null && h.waitStatus != 0) { //Wake up the successor node unparkSuccessor(h); } return true; } return false;}//Wake up the successor node private void unparkSuccessor(Node node) { //Get the waiting state of the given node int ws = node.waitStatus; //Update the waiting state to 0 if (ws < 0) { compareAndSetWaitStatus(node, ws, 0); } //Get the subsequent node of the given node Node s = node.next; //The successor node is empty or the waiting state is canceled if (s == null || s.waitStatus > 0) { s = null; //Finish the first node that is not canceled from the backward-forward traversal queue for (Node t = tail; t != null && t != node; t = t.prev) { if (t.waitStatus <= 0) { s = t; } } } //Wake up the first node after a given node that is not a cancel state if (s != null) { LockSupport.unpark(s.thread); }}After the thread holds the lock into the room, it will do its own business. After the work is done, it will release the lock and leave the room. The password lock can be unlocked through the tryRelease method. We know that the tryRelease method needs to be overwritten by the subclass. The implementation rules of different subclasses are different, which means that the passwords set by different subclasses are different. For example, in ReentrantLock, every time the person in the room calls the tryRelease method, the state will be reduced by 1, until the state is reduced to 0, the password lock will be opened. Think about whether this process looks like we are constantly turning the wheel of the password lock, and the number of wheels is reduced by 1 each time we rotate it. CountDownLatch is a bit similar to this one, except that it is not just that it is turning one person, but that it is going to turn one person around, concentrating everyone's strength to open the lock. After the thread leaves the room, it will find its original seat, that is, find the head node. See if anyone has left a small note for it on the seat. If there is, it will know that someone is asleep and needs to ask it to help wake it up, and then it will wake up that thread. If not, it means that no one is waiting in the synchronization queue for the time being, and no one needs it to wake up, so it can leave with peace of mind. The above process is the process of releasing the lock in exclusive mode.
Note: All the above analysis is based on JDK1.7, and there will be differences between different versions, readers need to pay attention.
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.