Source code analysis of AQS&ReentrantLock

Posted by brownca on Thu, 27 Jan 2022 09:08:39 +0100

There are two main methods to realize synchronization in Java: Synchronized and Lock. In the previous article, we introduced that Synchronized is an object Lock at the JVM level. Its implementation is based on the MESA management model, while Lock is implemented by Jdk. It is also based on the MESA management model. The core implementation of Lock is ReentrantLock.

1, AQS

1.1 what is AQS

stay Deep understanding of Synchronized In this article, the MESA management model has been introduced in detail. It is mainly composed of shared variables, entry waiting queue and condition queue. In the Synchronized implementation, there is only one condition queue, while in ReentrantLock, there can be multiple condition queues.

The core of Java implementation management model is abstractqueuedsynchronizer (AQS), which is an abstract class. It implements the logic of outgoing and incoming elements in waiting queue and condition queue, as well as the waiting wake-up mechanism of condition queue.

Most synchronizer implementations under Java's JUC package have common basic behaviors, such as waiting queue, conditional queue, exclusive acquisition, shared acquisition, etc. the abstractions of these behaviors are implemented in AbstractQueuedSynchronizer. AQS is an abstract synchronization framework that can be used to implement a state dependent synchronizer.

Most synchronizers in JDK are better than Lock, Latch, Barrier, etc. they use AQS. They basically inherit AQS by defining an internal class Sync, and then map all calls of the synchronizer to the corresponding methods of Sync

The following shows some AQS implementation classes:

1.2 AQS characteristics

AQS realizes the following basic functions:

  • Blocking waiting queue
  • Exclusive or shared
  • Fair or unfair
  • Reentrant
  • Interruptible

In AQS, a volatile int state attribute is maintained. This attribute indicates the available status of shared resources. Take ReentrantLock as an example, its lock() method will call AQS's tryAcquire() method, and this method is to try to modify the value of state attribute through CAS operation. If the modification is successful, it means that it has obtained the use qualification of shared resources, If the modification is unsuccessful, it will be added to the waiting queue.

The state attribute provides three ways to access and modify:

  • getState()

    protected final int getState() {
        return state;
    }
    
  • setState()

    protected final void setState(int newState) {
        state = newState;
    }
    
  • compareAndSetState()

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    

AQS defines two access modes for shared resources: exclusive and shared

  • Exclusive: only one thread can execute, such as ReentrantLock
  • Share: multiple threads can execute simultaneously, such as Semaphore/CountDownLatch

AQS inherits the AbstractOwnableSynchronizer. There is an exclusiveOwnerThread attribute in this class, which records the current exclusive thread. When judging whether it is reentry, it also calls the getExclusiveOwnerThread() method of this abstract class to obtain the exclusive thread and compare it with the current thread. If it is equal, it indicates that it is reentry and there is no need to retry to obtain the lock again.

AQS defines two types of queues in the MESA process model:

  • Synchronous waiting queue

    Used to maintain the thread queued when the lock acquisition fails

    AQS uses a two-way linked list to realize the synchronous waiting queue. The reason for using a two-way linked list is that AQS supports interruptibility. When a thread is interrupted, the two-way linked list can easily remove the thread from the waiting queue, while using a one-way linked list is more troublesome

  • Conditional waiting queue

    When the await() method of Condition is called, the lock will be released, and then the thread will be added to the Condition queue. When the sign() method is called to wake up, the thread nodes in the Condition queue will be synchronized into the queue and wait for the lock to be obtained again

    AQS uses a one-way linked list to implement the conditional waiting queue, which means that the threads in the conditional waiting queue cannot be interrupted

An internal class Node is defined in AQS. The Node encapsulates the Thread object, front and rear nodes (synchronization queue), Node status and the next waiting Node (waiting queue). The Node status is divided into five categories, in which the default value is 0.

  • Initialization status. The default value is 0, which indicates that the current node is in the synchronization queue and waiting to obtain the lock
  • CANCELLED status, with a value of 1, indicating that the current thread is canceled
  • SIGNAL status, with a value of - 1, indicates that threads in subsequent nodes of the current node in the synchronization queue can be executed, that is, call the unpark() method
  • CONDITION status, with a value of - 2, indicates that the current thread is in the conditional waiting queue
  • The state of PROPAGATE, with a value of - 3, indicates that subsequent acquireShared in the current scene can be executed

Different synchronizers compete for shared resources in different ways. When customizing the implementation of synchronizer (i.e. inheriting AQS), you only need to realize the acquisition and release of shared resource state according to specific requirements. As for the maintenance of specific threads in waiting queue and condition queue (failed to obtain lock, queue or wait for wake-up), AQS has been implemented.

When implementing the custom synchronizer, the following methods are mainly implemented. These methods are abstract methods in AQS:

  • isHeldExclusively()

    Judge whether the current thread monopolizes the shared resource state. It will be implemented only when Condition is used

  • tryAcquire(int arg)

    Exclusive mode attempts to obtain the shared resource state. If it succeeds, it returns true, and if it fails, it returns false

  • tryRelease(int arg)

    Exclusive mode attempts to release the shared resource state. If it succeeds, it returns true, and if it fails, it returns false

  • tryAcquireShared(int arg)

    When the sharing method tries to obtain the shared resource state, the negative number returned indicates failure, the 0 or positive number returned indicates success, and the return value is the number of remaining shared resources

  • tryReleaseShared(int arg)

    When the sharing mode attempts to release the shared resource state, it returns true for success and false for failure

1.3 synchronization waiting queue

The synchronous waiting queue in AQS, also known as CLH queue, is a queue based on two-way linked list data structure. It is a thread waiting queue in FIFO (first in first out) mode. The CLH queue in Java is a variant of the original CLH queue, and the thread is changed from the original spin mechanism to the blocking mechanism.

AQS relies on CLH synchronization queue to manage synchronization status:

  • If the current thread fails to obtain the synchronization status, AQS will form a node with the initialization status of the current thread and other information, add it to the CLH synchronization queue, and call locksupport Park () method to block the current thread.
  • When the synchronization state is released, the first node will be replaced (fair lock) to try to obtain the synchronization state again
  • Transfer the nodes in the Condition queue to the synchronization queue through the sign() or signAll() method of Condition

The basic structure is as follows:

When we introduced the Node structure earlier, it includes two front and rear Node pointers prev and next, which are used to block the queue to form a two-way linked list, while nextWaiter indicates the next Node pointer, which is used to form a one-way linked list for conditional waiting queue

1.4 conditional waiting queue

In AQS, the condition queue uses a one-way linked list to save nodes and uses the nextWaiter of nodes to connect

The Condition queue mainly includes await() and signal() methods, which are defined in the Condition interface. When the synchronizer needs to implement the Condition queue, it needs to implement these methods in the Condition interface

AQS implements the Condition interface through the internal class ConditionObject. In addition to implementing the above methods, addConditionWaiter() and other methods such as adding a waiting node are also defined

When await() method is called, the current thread will be encapsulated as a Node node, then added to the waiting queue and blocked

When the signal() method is called, the first node in the condition queue will be added to the tail of the synchronization queue to wait (from the condition queue to the synchronization queue)

When the thread in the synchronization queue header is awakened, the call await() method will block (from synchronous queue to conditional queue).

2, Detailed explanation of Condition interface method

1. Calling the Condition#await() method will release the lock held by the current thread, then block the current thread, and add a node to the end of the condition queue. Therefore, when calling the Condition#await() method, you must hold the lock

2. Calling the Condition#signal() method will move the first node of the condition queue to the end of the blocking queue, and then return to the original end node of the blocking queue (now the penultimate node). If the node is in cancelled status (canceled) or failed to update the status of the node to wake up status (SIGNAL), It will directly wake up the thread just inserted into the tail node of the blocking queue (that is, the thread blocked by calling the await() method).

The code is as follows. Later, we will further explain why the thread will wake up when the update to wake-up status fails:

Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
    LockSupport.unpark(node.thread);

After being awakened, the thread can compete for the lock, so when calling the Condition#signal() method, it must hold the lock. The thread holding the lock wakes up the thread blocked by calling the await() method

The following code shows the use of await() and signal() mechanisms

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

new Thread(() ->{
    lock.lock();
    try {
        log.info(Thread.currentThread().getName()+"Start executing");
        condition.await();
        log.info(Thread.currentThread().getName()+"Task completed");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        lock.unlock();
    }
},"Thread1").start();

new Thread(() -> {
    lock.lock();
    try {
        log.info(Thread.currentThread().getName()+"Start executing");
        condition.signal();
        Thread.sleep(2000);
        log.info(Thread.currentThread().getName()+"Task completed");
    } catch (InterruptedException e){
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
},"Thread2").start();

LockSupport.park();

The printed results are as follows:

17:11:12.735 [Thread1] INFO com.lizhi.lock.ReentrantLockTest - Thread1 Start executing
17:11:12.739 [Thread2] INFO com.lizhi.lock.ReentrantLockTest - Thread2 Start executing
17:11:14.748 [Thread2] INFO com.lizhi.lock.ReentrantLockTest - Thread2 Task completed
17:11:14.748 [Thread1] INFO com.lizhi.lock.ReentrantLockTest - Thread1 Task completed

In Ali's development specification manual, it is emphasized that the lock() method should be followed by the try finally code block, and the unlock() method should be called in the finally code block to prevent deadlock caused by code exceptions

3, ReentrantLock use

ReentrantLock is an implementation of AQS. It is a synchronization means to control thread concurrent access in JDK. Its function is similar to Synchronized. It is a mutual exclusion lock, which can ensure thread safety.

Compared with Synchronized, ReentranLock has the following features:

  • Interruptible
  • You can set the timeout time (condition queue)
  • Fair lock can be set (non fair by default)
  • Supports multiple conditional variables (Synchronized only supports one)
  • Like Synchronized, reentry is supported

Several core internal classes in ReentrantLock are as follows. The abstract class Sync implements AQS, while NonfairSync and FairSync correspond to the implementation of non fair lock and fair lock respectively. ReentrantLock adopts non fair lock by default.

The main difference between fair locks and non fair locks is that when a lock is added by non fair, it attempts to obtain the synchronization state through CAS. If it succeeds, it can continue to execute. If it fails, it calls the acquire() method to try to obtain the synchronization state, while for fair locks, it calls the acquire() method directly
[external chain picture transfer fails, and the source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-sdzrio6e-1643181214248) (? X-oss-process = image / watermark, type_d3f5lxplbmhlaq, shadow_50, text_q1netibac2vybw9ubgl6agk =, size_20, color_ffff, t_70, g_se, x_16#pic_center)]

There are some differences between Synchronized and ReentrantLock:

  • Synchronized is the lock implementation at the JVM level, while ReentrantLock is the lock implementation at the JDK level
  • The Synchronized lock status cannot be directly judged in the code, and the information of the object header needs to be allowed to be seen, while ReentrantLock can be judged through the isLocked() method
  • Synchronized is unfair, and ReentrantLock can be either unfair or fair
  • Synchronized cannot be interrupted, while the lockinterruptible () method of ReentrantLock can be interrupted
  • Synchronized will automatically release the lock when an exception occurs, while ReentrantLock requires the developer to follow the lock() method with the try finally code segment and display the release lock in the finally code segment
  • ReentrantLock has a variety of ways to obtain locks. In addition to the lock() method, you can also use the tryLock() method to return immediately and wait for the acquisition of a specified length of time
  • For the wake-up strategy, Synchronized, under certain circumstances, for the waiting thread, the later thread obtains the lock first, while ReentrantLock is the first thread to obtain the lock first

3.1 achieve synchronization

Basic use of ReentrantLock

ReentrantLock lock = new ReentrantLock(); //The default parameter is false, which is unfair  
ReentrantLock lock = new ReentrantLock(true); //Fair lock  

//Lock    
lock.lock(); 
try {  
    //Critical zone 
} finally { 
    // Unlock 
    lock.unlock();  
}

The following code demonstrates the effect of synchronization

private static  int sum = 0;
private static Lock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {

    for (int i = 0; i < 3; i++) {
        Thread thread = new Thread(()->{
            lock.lock();
            try {
                for (int j = 0; j < 10000; j++) {
                    sum++;
                }
            } finally {
                lock.unlock();
            }
        });
        thread.start();
    }
    Thread.sleep(2000);
    System.out.println(sum);
}

3.2 reentrant

public static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
    method1();
}


public static void method1() {
    lock.lock();
    try {
        log.debug("execute method1");
        method2();
    } finally {
        lock.unlock();
    }
}
public static void method2() {
    lock.lock();
    try {
        log.debug("execute method2");
        method3();
    } finally {
        lock.unlock();
    }
}

3.3 interruptible

The lockinterruptible () method will judge whether it is interrupted during the spin attempt to obtain the synchronization status. If the thread calls the interrupt() method, the lockinterruptible () method will throw an interrupt exception

ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {

    log.debug("t1 start-up...");

    try {
        lock.lockInterruptibly();
        try {
            log.debug("t1 Got the lock");
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
        log.debug("t1 The process of waiting for the lock is interrupted");
    }

}, "t1");

lock.lock();
try {
    log.debug("main The thread acquired the lock");
    t1.start();
    //Let thread t1 execute first
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.interrupt();
    log.debug("thread  t1 Execution interrupt");
} finally {
    lock.unlock();
}

3.4 try to lock

3.4.1 immediate failure

The tryLock() method attempts to obtain the synchronization status through CAS. If it fails, it will return directly and will not put the current thread into the waiting queue for blocking

ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {

    log.debug("t1 start-up...");
    // Note: even if the fair lock is set, this method will immediately return the success or failure of obtaining the lock, and the fair policy will not take effect
    if (!lock.tryLock()) {
        log.debug("t1 Failed to acquire lock, return immediately false");
        return;
    }
    try {
        log.debug("t1 Got the lock");
    } finally {
        lock.unlock();
    }

}, "t1");


lock.lock();
try {
    log.debug("main The thread acquired the lock");
    t1.start();
    //Let thread t1 execute first
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
} finally {
    lock.unlock();
}

3.4.2 timeout failure

If the tryLock() method specifies a timeout time, the current thread will be put into the synchronization waiting queue first during execution, and then spin to try to obtain the synchronization status. If the synchronization status is still not obtained within the specified timeout time, it will directly return false. In the process of spinning, the thread is allowed to interrupt. At the same time, if it can be executed in the synchronization queue, but its timeout is greater than 1000ns, it will be blocked by calling the parkNanos() method, even if its timeout

ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
    log.debug("t1 start-up...");
    //overtime
    try {
        if (!lock.tryLock(1, TimeUnit.SECONDS)) {
            log.debug("Wait 1 s Failed to acquire lock after. Return");
            return;
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
        return;
    }
    try {
        log.debug("t1 Got the lock");
    } finally {
        lock.unlock();
    }

}, "t1");


lock.lock();
try {
    log.debug("main The thread acquired the lock");
    t1.start();
    //Let thread t1 execute first
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
} finally {
    lock.unlock();
}

4, ReentrantLock source code analysis

ReentrantLock is the most important lock implementation of JDK. Many synchronizers are implemented on the basis of ReentrantLock, and it is often used as a mutually exclusive tool in development. Next, we will learn how to realize synchronization by combining the source code of lock() and unlock().

4.1 Lock method

ReentrantLock provides two implementation classes: Fair lock and non fair lock. The default is non fair lock (which can provide performance and avoid unnecessary blocking waiting). Its two construction methods are as follows:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

Next, take the fair lock as an example to introduce the logic of ReentrantLock locking. The lock() method mainly calls the acquire() method. For non-public flat locks, the acquire() method will also be called after the attempt to obtain synchronization fails. The basic logic is the same

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }
    ......
}

The acquire() method has been implemented in AQS. It first attempts to obtain the synchronization status through the tryAcquire() method. If it fails, it calls the addWaiter() method to encapsulate the thread as a Node and add it to the synchronization blocking queue, and then calls the acquirequeueueueueueued () method to block. In the blocking method, it will judge the interrupt status of the current thread. If it is an interrupt status, The interrupt method of the thread will be called.

The specific logic is analyzed slowly in combination with the source code

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

4.1.1 trying to get synchronization status

AQS only provides the definition of the tryAcquire() method, and the specific implementation is left to the implementation class. The following is the implementation in FairSync

As mentioned earlier when introducing AQS, AQS synchronizes through the state attribute value. In ReentrantLock, the initial value of the state attribute is 0. Trying to obtain the synchronization status is to change the state attribute value from 0 to 1 through CAS, and releasing the synchronization status is to change it back to 0

The following code first judges the current state attribute value. If it is not 0, it indicates that a thread has obtained the synchronization status. Because ReentrantLock supports reentrant, the else branch below will compare the current thread with the thread holding the synchronization status. If it is the same thread, it indicates that it is a reentry operation. You only need to set the state attribute value to the new value

If the state attribute value is 0, there is a hasQueuedPredecessors() method here, which is also the core difference between fair locks and non fair locks. This method will judge whether there are threads waiting in the current synchronization queue and there is no current thread in the waiting threads (i.e. not reentrant). Only when the above conditions are met, will it try to modify the state attribute value through CAS, Then set the current thread as the exclusive thread.

For fair lock, its fairness is reflected in that as long as there are other threads waiting in the synchronization queue, the new thread must also wait in the queue

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

4.1.2 add to blocking queue

If the attempt to obtain the synchronization status fails, the next step is to call the addWaiter() method to encapsulate the current thread into a Node node, and then add it to the synchronization queue to wait. For ReentrantLock, its Node type is exclusive

addWaiter(Node.EXCLUSIVE); //Add a node of exclusive type

In AQS, exclusive and shared types are represented by two Node attributes, and then recorded by nextWaiter

static final class Node {
    /** 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;
}

In the following method, first, the current thread is encapsulated into a Node node according to the node type. If the tail node is not null, the current node is set to the tail node directly through CAS, and then the pointer to the front end node is adjusted.

If the setting of the tail node fails or the tail node is null, the enq() method is called to create a waiting queue (two-way linked list) or add a node

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

enq() method is a very classic way to create a two-way linked list in a concurrent environment. It also judges the tail Node first. If the tail Node is null, it means that the head Node is also null at this time, which is not a two-way linked list structure. At this time, you can create a Node as the head Node through CAS, and then the tail Node also points to this Node, At this point, the Node is both a head Node and a tail Node.

Then, combined with the spin, set the current node as the tail node through the compareAndSetTail() method, and finally exit the spin.

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

4.1.2 update node status and block

The addWaiter() method just encapsulates the current thread into a Node node and adds it to the blocking queue. At this time, the thread has not been blocked, and the thread blocking is completed in the acquirequeueueueueueueueued () method. This method will not only block the thread, but also try to obtain the synchronization status for the thread.

The source code is as follows:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

The focus of this method is for(;) It can ensure that the thread of the current node can obtain the synchronization state to execute the business logic.

First, call the predconnector () method of the current node to obtain the front node. If the front node is the head node, call the tryAcquire() method to try to obtain the synchronization status. After successful acquisition, the node will be removed from the synchronization queue. The removal method is also simple. Call the setHead() method to take the current node as the head node, Then set the Thread and prev attributes of the current node to null, and then change the next attribute of the previous header node to null, which is equivalent to being in the current node.

If the front node is not the head node or the attempt to obtain the synchronization status fails, go back and call the shouldParkAfterFailedAcquire() method in the if below. This method is mainly used to judge the status of the front node.

If the status is SIGNAL, it means that the subsequent nodes will be awakened, so return true directly, and then call the parkAndCheckInterrupt() method to block the thread (that is, the premise of blocking must be that it can be awakened, that is, the status of its predecessor node is SIGNAL)

The implementation of shouldParkAfterFailedAcquire() is also relatively simple. If the status of the front node is SIGNAL, it will be returned directly

If the status is greater than 0, that is, the CANCELLED status, all the preceding nodes with the status of CANCELLED will be removed from the synchronization queue

If it is in other states, CAS is used to change the state of the front node to SIGNAL state, and finally false is returned

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

In acquirequeueueueued(), after shouldParkAfterFailedAcquire() method returns false, it will continue to spin. When shouldParkAfterFailedAcquire method returns true, it means that the thread of the current node can be blocked, and then it will call parkAndCheckInterrupt() method to block

Blocking is accomplished through LockSupport's park() method. Thread blocking at JDK level is realized in this way.

Thread. Is called when the thread is awakened The interrupted () method is used to determine whether the thread is interrupted. Because this method will reset the interrupt flag bit, the acquirequeueueueueueueueued () method will change interrupted to true, and then call the selfInterrupt() method in the above acquire() method. This method is to reset the interrupt flag bit to false

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

Summary: it can be seen from the above steps that after calling the lock() method, the current thread will be encapsulated as a Node and added to the synchronization queue. If its front Node is not the head Node or the attempt to obtain the synchronization status fails, the state of the front Node will be changed to SIGNAL, and then the current thread will be blocked.

4.2 unlock method

4.2.1 attempt to release

The unlock() method of ReentrantLock is to call the release() method, which has been implemented in AQS, but the tryRelease() method in the method is implemented by the Sync class of ReentrantLock's internal class

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

This method is also very simple. The input parameter of the unlock() method is 1 because it is an exclusive lock. In the tryRelease() method, the state attribute value defined in AQS will be - 1. Because the exclusive lock supports reentry, the state attribute value may be greater than 1, and it will be released only when the attribute value is 0.

When it is necessary to release, change the exclusive thread to null, and then return true, indicating that the synchronization state can be released.

Whether the synchronization state can be released or not, the attribute value of state needs to be updated

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

4.2.2 wake up thread

If the attempt to release the synchronization status is successful, get the head node of the synchronization queue and call the unparksuccess () method

If the state of the head node is SIGNAL, change its state to 0 (initial state), and then obtain the next node of the head node (the head node is not the node of a thread).

If the node is null or the node state is CANCELLED, start from the tail node of the synchronization queue and traverse forward until you find the node whose first state is SIGNAL or initialization. If the node is not null, wake up the thread in the node

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

Topics: Java JUC