Through the analysis in the previous article, we know that there are three ways to acquire locks with exclusive mode, namely, to obtain without response thread interrupts, to obtain response thread interrupts, and to obtain timeout time. There are also these three ways to acquire locks in shared mode, and they are basically the same. If we figure out one way, we can quickly understand other ways. Although the AbstractQueuedSynchronizer source code has more than a thousand lines, it is also repeated many times, so readers should not be scared at the beginning. Just read it patiently and slowly, you will naturally gradually understand it. In my personal experience, there are several more critical aspects to understand when reading the AbstractQueuedSynchronizer source code, namely the difference between exclusive mode and shared mode, the waiting state of nodes, and the understanding of conditional queues. If you understand these key points, then the subsequent source code reading will be much easier. Of course, these are introduced in my article "Java Concurrency Series [1]----AbstractQueuedSynchronizer Source Code Analysis", and readers can check it out first. This article analyzes the sharing mode into three ways to acquire locks and one way to release locks.
1. Not responding to thread interrupt acquisition
//Acquiring the lock in non-interruptible mode (shared mode) public final void acquireShared(int arg) { //1. Try to acquire the lock if (tryAcquireShared(arg) < 0) { //2. If the acquisition fails, enter this method doAcquireShared(arg); }}//Try to acquire the lock (shared mode)//Negative number: indicates that the acquisition failed//Zero value: indicates that the current node is successfully acquired, but the successor node can no longer obtain //Positive number: indicates that the current node is successfully acquired, and the successor node can also obtain success protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException();}Calling the acquireShared method is a way to acquire the lock without responding to thread interrupts. In this method, tryAcquireShared is first called to try to acquire the lock. The tryAcquireShared method returns a state of acquiring the lock. Here AQS specifies that if the return status is negative, it means that the current node fails to acquire the lock. If 0 means that the current node acquires the lock, but the subsequent node cannot be acquired anymore. If it is positive, it means that the current node acquires the lock, and the subsequent nodes of this lock can also be obtained successfully. When a subclass implements the logic of obtaining locks by the tryAcquireShared method, the return value needs to comply with this convention. If the return value of calling tryAcquireShared is less than 0, it means that the attempt to acquire the lock failed. Next, call the doAcquireShared method to add the current thread to the synchronization queue. We see the doAcquireShared method.
//Get (shared mode) in the synchronization queue private void doAcquireShared(int arg) { //Add to the synchronization queue final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { //Get the forward node of the current node final Node p = node.predecessor(); //If the forward node is a head node, try to acquire the lock again if (p == head) { //Try to acquire the lock again and return the acquisition status //r < 0, indicating that the acquisition failed //r = 0, indicating that the current node is successfully acquired, but the subsequent node cannot be acquired anymore //r > 0, Indicates that the current node is successfully acquired, and the successor node can also be successfully acquired int r = tryAcquireShared(arg); if (r >= 0) { //To this end, it indicates that the current node has successfully acquired the lock. At this time, it will propagate the lock status information to the subsequent node setHeadAndPropagate(node, r); p.next = null; //If an interrupt request is received during thread blocking, respond to the request at this step if (interrupted) { selfInterrupt(); } failed = false; return; } } //Every time the lock acquisition fails, it will determine whether the thread can be suspended. If it is possible, the thread will be suspended in the parkAndCheckInterrupt method if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { interrupted = true; } } } finally { if (failed) { cancelAcquire(node); } }}Entering the doAcquireShared method first, call the addWaiter method to wrap the current thread into a node and put it at the end of the synchronization queue. We have talked about the process of adding nodes when talking about the exclusive mode, so I won’t talk about it here. After a node enters the synchronization queue, if it finds that the node in front of it is the head node, because the thread of the head node has acquired the lock and entered the room, then it is its turn to acquire the lock. Therefore, the current node will not hang itself first, but will try to acquire the lock again. If the person in front just releases the lock and leaves, the current node can successfully obtain the lock. If the person in front has not released the lock, it will call the shouldParkAfterFailedAcquire method. In this method, the state of the head node will be changed to SIGNAL. Only by ensuring that the state of the previous node is SIGNAL, the current node can hang itself with confidence. All threads will be suspended in the parkAndCheckInterrupt method. If the current node happens to successfully acquire the lock, then the setHeadAndPropagate method will be called to set itself as the head node and wake up the node that is also shared mode behind. Let's take a look at the specific operation of the setHeadAndPropagate method.
//Set the head node and propagate the state of the lock (shared mode) private void setHeadAndPropagate(Node node, int propagate) { Node h = head; //Set the given node as the head node setHead(node); //If propagate is greater than 0, it means that the lock can obtain if (propagate > 0 || h == null || h.waitStatus < 0) { //Get the successor node of the given node Node s = node.next; //If the successor node of the given node is empty, or its state is a shared state if (s == null || s.isShared()) { //Wake up the successor node doReleaseShared(); } }}//Release lock operation (shared mode) private void doReleaseShared() { for (;;) { //Get the head node of the synchronous queue Node h = head; if (h != null && h != tail) { //Get the waiting state of the head node int ws = h.waitStatus; //If the status of the head node is SIGNAL, it means that someone is queuing behind if (ws == Node.SIGNAL) { //Get the waiting state of the head node to 0 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) { continue; } //Wake up the successor node unparkSuccessor(h); //If the status of the head node is 0, it means that no one is queuing later, just modify the head state to PROPAGATE }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) { continue; } } //Only by ensuring that the head node has not been modified during the period, you can break out of the loop if (h == head) { break; } }}Calling the setHeadAndPropagate method first sets itself as the head node, and then decides whether to wake up the successor node based on the return value of the passed tryAcquireShared method. As mentioned earlier, when the return value is greater than 0, it means that the current node has successfully acquired the lock, and the subsequent node can also successfully acquire the lock. At this time, the current node needs to wake up the node that is also in the shared mode. Note that each time you wake up, it is only to wake up the next node. If the latter node is not in the shared mode, the current node will directly enter the room and will not wake up the further node. The operation of waking up successor nodes in shared mode is performed in the doReleaseShared method. The wake-up operations of shared mode and exclusive mode are basically the same. Both find the brand on your seat (waiting state). If the brand is SIGNAL, it means that someone needs to help wake it up later. If the brand is 0, it means that no one is queuing in the queue at this time. In exclusive mode, if you find that no one is queuing, you will leave the queue directly. In shared mode, if you find that no one is queuing behind the queue, the current node will still leave a small note before leaving (set the waiting status to PROPAGATE) to tell the later people the available state of this lock. Then when the person who comes later can judge whether to directly acquire the lock based on this state.
2. Response to thread interrupt acquisition
//Acquiring the lock in interruptible mode (shared mode) public final void acquireSharedInterruptibly(int arg) throws InterruptedException { //First determine whether the thread is interrupted, if so, throw an exception if (Thread.interrupted()) { throw new InterruptedException(); } //1. Try to acquire the lock if (tryAcquireShared(arg) < 0) { //2. If the acquisition fails, enter this method doAcquireSharedInterruptibly(arg); }}//Acquiring in interruptible mode (shared mode) private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //Insert the current node into the tail of the synchronization queue final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //Get the previous node final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { //If the thread receives an interrupt request during the blocking process, it will immediately throw an exception here throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } }}The way of acquiring a lock in response to thread interrupts and the way of acquiring a lock in response to thread interrupts is basically the same in the process. The only difference is where to respond to thread interrupt requests. When the thread interrupt is not responding to the thread interrupt to acquire the lock, the thread is awakened from the parkAndCheckInterrupt method. After the wake-up, it immediately returns whether the interrupt request has been received. Even if the interrupt request is received, it will continue to spin until it has been acquired until it responds to the interrupt request and hangs itself. The thread will immediately respond to the interrupt request after the thread is awakened. If the thread interrupt is received during the blocking process, an InterruptedException will be immediately thrown.
3. Set the timeout time to obtain
//Acquiring the lock with a limited timeout (shared mode) public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) { throw new InterruptedException(); } //1. Calling tryAcquireShared to try to acquire the lock//2. If the acquisition fails, call doAcquireSharedNanos return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);}//Acquiring the lock with a limited timeout (shared mode) private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException { long lastTime = System.nanoTime(); final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //Get the previous node of the current node final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; failed = false; return true; } } //If the timeout is used up, the acquisition will be terminated and the failure information will be returned if (nanosTimeout <= 0) { return false; } //1. Check whether the thread suspension requirement is met (guaranteed the forward node status is SIGNAL) //2. Check whether the timeout time is greater than the spin time if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) { //If the above two conditions are met, the current thread will be suspended for a period of time LockSupport.parkNanos(this, nanosTimeout); } long now = System.nanoTime(); //Timeout time every time subtracts the time of the lock acquisition nanosTimeout -= now - lastTime; lastTime = now; //If an interrupt request is received during blocking, an exception will be immediately thrown if (Thread.interrupted()) { throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } }}If you understand the above two acquisition methods, it will be very easy to set the acquisition method of timeout time. The basic process is the same, mainly to understand the timeout mechanism. If the lock is acquired for the first time, the doAcquireSharedNanos method will be called and the timeout time will be passed in. After entering the method, the lock will be acquired again according to the situation. If the lock fails again, the thread must be considered to be suspended. At this time, we will determine whether the timeout time is greater than the spin time. If so, the thread will be suspended for a period of time. Otherwise, we will continue to try to obtain it. After each time we acquire the lock, we will subtract the time of the lock to acquire it. We will loop like this until the timeout time is exhausted. If the lock has not been acquired, the acquisition will be terminated and the acquisition failure flag will be returned. The thread responds to thread interrupts throughout the period.
4. Dequeuing operations of nodes in shared mode
//Operation of releasing lock (shared mode) public final boolean releaseShared(int arg) { //1.Try to release the lock if (tryReleaseShared(arg)) { //2. If the release is successful, wake up other threads doReleaseShared(); return true; } return false;}//Try to release the lock (shared mode) protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException();}//Operation of releasing lock (shared mode) private void doReleaseShared() { for (;;) { //Get the head node of the synchronous queue Node h = head; if (h != null && h != tail) { //Get the waiting state of the head node int ws = h.waitStatus; //If the status of the head node is SIGNAL, it means that someone is queuing later if (ws == Node.SIGNAL) { //First update the waiting state of the head node to 0 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) { continue; } //Wake up the subsequent node unparkSuccessor(h); //If the status of the head node is 0, it means that no one is queuing later, it just changes the head state to PROPAGATE }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) { continue; } } //The loop can only be broken if (h == head) { break; } }}After the thread finishes work in the room, it will call the releaseShared method to release the lock. First, it will call the tryReleaseShared method to try to release the lock. The judgment logic of this method is implemented by the subclass. If the release is successful, call the doReleaseShared method to wake up the successor node. After walking out of the room, it will find the original seat (head node) and see if anyone has left small notes on the seat (status SIGNAL). If so, wake up the successor node. If there is no (status 0) means that no one is queuing in the queue, then the last thing it has to do before leaving is to leave, which is to leave a small note on its seat (status is set to PROPAGATE) to tell the people behind the lock to acquire the state. The only difference between the entire lock release process and the exclusive mode is to operate in this last step.
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.