011Java and 015AQS

Keywords: Java

Note: This article is based on JDK1.8.

1 Introduction

1.1 what is it

AQS is the abbreviation of the English word AbstractQueuedSynchronizer, which translates into an abstract queue synchronizer. AQS defines a synchronizer framework for multi-threaded access to shared resources. Many synchronization class implementations depend on it, such as ReentrantLock, Semaphore, CountDownLatch, etc.

AQS is a heavyweight basic framework used to build locks or other synchronizer components and the cornerstone of the whole JUC system. It completes the queuing of resource acquisition threads through the built-in FIFO queue, and represents the state of holding locks through a state integer variable.

1.2 abstraction

The main use of AQS is inheritance. Subclasses manage the synchronization state by inheriting the synchronizer and implementing its abstract methods.

1.3 principle

The thread that grabs the resource directly uses the processing business logic. If it cannot grab the resource, it must involve a queuing mechanism. When it comes to the queuing mechanism, there must be some kind of queue. What data structure is such a queue.

If shared resources are occupied, a certain blocking waiting wake-up mechanism is needed to ensure lock allocation. This mechanism is mainly implemented by a variant of CLH queue. Threads that cannot obtain locks temporarily are added to the queue. This queue is the abstract representation of AQS. It encapsulates the thread requesting shared resources into the Node node of the queue, and maintains the state of the state variable through the credential mechanism of CAS, spin and LockSupport, so as to achieve the synchronization control effect of concurrency.

CLH: Craig, Landin and Hagersten (names of three scientists). The original version is a one-way linked list. The queue in AQS is a virtual two-way queue FIFO of CLH variant, and its head node becomes an empty node after initialization.

1.4 resource usage

AQS defines two resource usage modes: Exclusive (Exclusive, only one thread can execute, such as ReentrantLock) and Share (shared, multiple threads can execute at the same time, such as Semaphore/CountDownLatch).

2 Architecture

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    // Internal encapsulated Node
    static final class Node {
        // The tag thread waits for a lock in shared mode
        static final Node SHARED = new Node();
        // The tag thread waits for a lock in exclusive mode
        static final Node EXCLUSIVE = null;
        // The value of waitStatus is 1, which means that the thread is cancelled (timeout, interrupt), and the cancelled node will not block
        static final int CANCELLED =  1;
        // The value of waitStatus is - 1, indicating that the successor node is ready to complete and wait for the thread to release resources
        static final int SIGNAL    = -1;
        // The value of waitStatus is - 2, which means that the thread is blocked in the Condition queue. When other threads call the wake-up method in the Condition, the node will be transferred from the Condition queue to the CLH waiting queue (used in the Condition)
        static final int CONDITION = -2;
        // The value of waitStatus is - 3, indicating unconditional propagation of threads and subsequent threads (shared mode is available, and CountDownLatch is used)
        static final int PROPAGATE = -3;
        // The waiting state of the thread. The initial value is 0
        volatile int waitStatus;
        // Precursor node
        volatile Node prev
        // Successor node
        volatile Node next;
        // Thread object
        volatile Thread thread;
        ...
    }
    // Head node
    private transient volatile Node head
    // Tail node
    private transient volatile Node tail;
    // Resource status, 0 means available, and greater than or equal to 1 means occupied
    private volatile int state;
    // Get resource status
    protected final int getState() {
        return state;
    }
    // Set resource status
    protected final void setState(int newState) {
        state = newState;
    }
    // CAS set resource status
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    ...
}

AQS uses a volatile modified integer variable state to represent the synchronization status, and completes the queuing of threads through the built-in CLH synchronization queue.

Among them, the modification of state value is completed through CAS. 0 indicates that the resource is available, and greater than or equal to 1 indicates that the resource is unavailable. AQS provides three methods for operating state: getState(), setState(), compareAndSetState().

The current thread determines whether it can obtain resources according to the value of state. If the acquisition fails, AQS will encapsulate the current thread thread and waiting status waitStatus into Node nodes, add them to the CLH into the synchronization queue, and block the current thread at the same time. When the value of state becomes available, the thread in the Node will wake up and try to obtain resources again.

3 Lock and AQS

The implementation classes of Lock interface basically complete thread access control by aggregating a subclass of queue synchronizer.

public class ReentrantLock implements Lock, java.io.Serializable {
    ...
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
        ...
    }
    static final class NonfairSync extends Sync {
        ...
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    static final class FairSync extends Sync {
        ...
        final void lock() {
            acquire(1);
        }
        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;
        }
    }
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    ...
}

A Sync class is aggregated inside the ReentrantLock class. The Sync class inherits the AQS class, and both non fair lock NonfairSync and fair lock FairSync inherit from Sync. The non fair lock NonfairSync is created by default.

4 analyze ReentrantLock

4.1 General

The whole locking process of ReentrantLock can be divided into three stages:

1) Try locking.

2) Locking failed. The thread is queued.

3) After the thread enters the queue, it enters the blocking state.

4.2 scenario examples

For example, three customers handle business in the bank and use the default unfair lock:

public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    new Thread(()->{
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "-----Handle the business");
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-----leave");
        } finally {
            lock.unlock();
        }
    }, "A").start();
    new Thread(()->{
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "-----Handle the business");
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-----leave");
        } finally {
            lock.unlock();
        }
    }, "B").start();
    new Thread(()->{
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "-----Handle the business");
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-----leave");
        } finally {
            lock.unlock();
        }
    }, "C").start();
}

5 program analysis

5.1 thread A starts and executes

5.1.1 access to resources

Thread A enters and calls the lock() method to view the implementation:

final void lock() {
    // Use CAS to set state to 1
    if (compareAndSetState(0, 1))
        // Indicates that the resource is obtained successfully. Set the current thread as the occupied thread
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // Indicates that the acquisition of resources failed and continues to preempt resources
        acquire(1);
}

Because thread A is the first thread to obtain resources, it is successfully set by using the compareAndSetState() method. Continue to call setExclusiveOwnerThread() method to set the current thread as the occupying thread, and then continue to execute business.

5.2 thread B starts and blocks

5.2.1 access to resources

Thread B enters and calls the lock() method.

Because thread B is the second thread to obtain resources, and thread A has changed the state from 0 to 1, setting using compareAndSetState() method fails. Continue to call acquire() method to obtain resources. Check the implementation:

public final void acquire(int arg) {
    // Seize resources
    if (!tryAcquire(arg) &&
        // Join the waiting queue
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // Thread blocking
        selfInterrupt();
}

If the resource preemption is successful, call the tryAcquire() method to return true, judge the end of the condition and continue the business.

If the preemption of resources fails, continue to judge whether the acquirequeueueueueueueueueued () method returns. Execute the addWaiter() method and pass in the parameter to add the thread to the waiting queue in exclusive mode.

5.2.2 preemption of resources

Thread B enters, continues to call the acquire() method to obtain resources, executes the tryAcquire() method, and view the implementation:

protected final boolean tryAcquire(int acquires) {
    // Continue to call the attempted preemption method of the unfair lock
    return nonfairTryAcquire(acquires);
}

Continue to call the nonfairTryAcquire() method of the non fair lock, and return false, indicating that the occupation failed:

final boolean nonfairTryAcquire(int acquires) {
    // Record current thread
    final Thread current = Thread.currentThread();
    // Record current resource status
    int c = getState();
    // 0 indicates that the current resource is available
    if (c == 0) {
        // Use CAS to set state to the number of requests
        if (compareAndSetState(0, acquires)) {
            // Indicates that the resource is obtained successfully. Set the current thread as the occupied thread
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // Greater than or equal to 1 indicates that the current resource is occupied. Judge whether the current thread is an occupied thread (in the case of reentrant lock)
    else if (current == getExclusiveOwnerThread()) {
        // The current thread is an occupied thread, and the resource status is recorded
        int nextc = c + acquires
        // Determine whether overflow
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // Set state to the new resource state
        setState(nextc);
        return true;
    }
    return false;
}

5.2.3 entry waiting

Thread B enters and continues to call the addWaiter() method to add the current thread to the waiting queue. Check the implementation:

private Node addWaiter(Node mode) {
    // Encapsulates the current thread and the incoming exclusive mode as nodes
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // If the tail node is not empty, it indicates that the CLH queue has been initialized, and the CAS operation sets the current node as the tail node
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // If the tail node is empty, it means that the CLH queue has not been initialized, and the queue has not been initialized
    enq(node);
    return node;
}

Because thread B is the first thread to enter the waiting, and the tail node is empty, continue to check the enq() method:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // If the tail node is empty, set the head node and tail node to be empty through CAS
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // The tail node is not empty. Take the current node as a new tail node through CAS
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

After initializing the CLH queue, the head node is empty and the tail node is the current node.

5.2.4 blocking threads

After thread B obtains the current node, it passes in the acquirequeueueueued() method as a parameter to continue execution:

final boolean acquireQueued(final Node node, int arg) {
    // Records whether the current node is cancelled. The default value is true, indicating cancellation
    boolean failed = true;
    try {
        // Mark whether the current node is interrupted. The default value is false, indicating that the current node is not interrupted
        boolean interrupted = false
        // spin
        for (;;) {
            // Gets the previous node of the current node
            final Node p = node.predecessor();
            // If the current node is the head node, it means that the current node is about to wake up and try to preempt resources
            if (p == head && tryAcquire(arg)) {
                // Set the current node as the head node, empty the previous node of the current node, and unbind the current node from the current thread
                setHead(node);
                // Empty the next node of the original header node to facilitate GC recycling
                p.next = null; // help GC
                // Mark the current node as false, indicating that it has not been cancelled
                failed = false;
                // Returns false, indicating that the current node is not interrupted
                return interrupted;
            }
            // No matter whether the current node is the head node or not, the execution here means that the acquisition of resources fails. The front node is processed and the current node is blocked
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // Marked as true indicates that the current node is interrupted
                interrupted = true;
        }
    } finally {
        // If the current node is cancelled, cancel the operation
        if (failed)
            cancelAcquire(node);
    }
}

Because thread B is the first thread to enter the wait, the previous node is the head node and attempts to obtain resources. If the acquisition is successful, take the current node as the head node and remove the current thread. If the acquisition fails, enter judgment.

Call the shouldParkAfterFailedAcquire() method in the judgement condition to handle the front node:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // Record the waiting status of the previous node
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // If the waiting status of the previous node is - 1, it indicates that the current thread can be blocked, returns true, and executes the parkAndCheckInterrupt() method
        return true;
    if (ws > 0) {
        // If the waiting state of the previous node is 1, it means that the previous node is cancelled, and the cancelled previous node is removed in a circular manner
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // If the above conditions are not met, it means that the waiting state of the previous node is 0 or - 3, and the waiting state is set to - 1 through CAS
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // Return false, skip the parkAndCheckInterrupt() method and re-enter the spin
    return false;
}

Because thread B is the first thread to enter the waiting, the previous node is the head node, the head node is an empty node, and the waiting state is 0, it enters this method twice.

After entering the shouldParkAfterFailedAcquire() method for the first time, set the waiting state of the previous node to - 1 and return false. The condition is judged to be false and re-enter the spin.

After entering shouldParkAfterFailedAcquire() method for the second time, it detects that the waiting state of the previous node is - 1, returns true, and continues to judge the parkAndCheckInterrupt() method.

Call the parkAndCheckInterrupt() method in the judgement condition to block the current node:

private final boolean parkAndCheckInterrupt() {
    // Use the park() method of LockSupport to block the current node
    LockSupport.park(this);
    // Returns the interrupt status of the thread
    return Thread.interrupted();
}

Thread B is blocked here.

5.3 thread C starts and blocks

5.3.1 access to resources

Thread C enters and calls the lock() method.

Because thread C is the third thread to obtain resources, and thread A has changed the state from 0 to 1, setting with compareAndSetState() method fails. Continue to call acquire() method to obtain resources.

If the resource preemption is successful, call the tryAcquire() method to return true, judge the end of the condition and continue the business.

If the preemption of resources fails, continue to judge whether the acquirequeueueueueueueueueued () method returns. Execute the addWaiter() method and pass in the parameter to add the thread to the waiting queue in exclusive mode.

5.3.2 preemption of resources

Thread C enters, continues to call the acquire() method to obtain resources, and executes the tryAcquire() method.

Continue to call the nonfairTryAcquire() method of the unfair lock, and return false, indicating that the occupation failed.

5.3.3 entry waiting

Thread C enters and continues to call the addWaiter() method to add the current thread to the waiting queue.

Because thread C is the second thread entering the waiting, thread B has completed the queue initialization, and the tail node is not empty. Take the current node as the new tail node.

5.3.4 blocking threads

After thread C obtains the current node, it passes in the acquirequeueueueued () method as a parameter to continue execution.

Because thread C is the second thread to enter the waiting, the previous node is not the head node and directly enters the judgment.

The shouldParkAfterFailedAcquire() method is invoked in the judgment condition, the front node is processed, the waiting state of the B node is set to -1, and true is returned, and the parkAndCheckInterrupt() method is continued to be judged.

The parkAndCheckInterrupt() method is called in the judgement condition to block the current node.

Thread C is blocked here.

5.4 end of thread A

5.4.1 unlocking resources

After thread A completes execution, call the unlock() method to release resources and wake up the thread. View the implementation:

public void unlock() {
    sync.release(1);
}

Continue to view the release() method:

public final boolean release(int arg) {
    // The tryRelease() method is called to attempt to release the resource
    if (tryRelease(arg)) {
        // Get header node
        Node h = head;
        // If the header node is not empty and the wait state is not 0, it indicates that other threads need to be awakened
        if (h != null && h.waitStatus != 0)
            // Call the unparksuccess () method and pass in the header node to wake up the thread
            unparkSuccessor(h);
        return true;
    }
    // Release failed, return false
    return false;
}

5.4.2 releasing resources

Continue to view the tryRelease() method:

protected final boolean tryRelease(int releases) {
    // Record resource status
    int c = getState() - releases;
    // If the current thread is not an occupying thread, an exception is thrown
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // Mark that the resource is idle. The default value is false
    boolean free = false
    // If the resource status is 0, mark the resource idle as true and set the occupied thread empty
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // Set resource status
    setState(c);
    // Return resource idle
    return free;
}

Thread A releases resources and returns true to continue execution.

5.4.3 wake up thread

Since thread B and thread C have entered the waiting queue, the header node is not empty. Continue to view the unparksuccess() method:

private void unparkSuccessor(Node node) {
    // Record the waiting status of the header node
    int ws = node.waitStatus;
    // If the waiting state of the head node is less than 0, set the waiting state of the head node to 0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // Record the next node of the header node
    Node s = node.next;
    // Judge whether the next node is empty or whether the waiting state of the next node is greater than 0
    if (s == null || s.waitStatus > 0) {
        s = null;
        // Traverse the next node, find the node that is not empty and the waiting state is less than or equal to 0, and set it as the next node
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // If the next node is not empty, use the unpark() method of LockSupport to wake up the thread in the next node
    if (s != null)
        LockSupport.unpark(s.thread);
}

The next node of the head node is the node where thread B is located, and thread B is awakened.

5.5 thread B executes and ends

5.5.1 preemption of resources

After thread B is released in the parkAndCheckInterrupt() method, it returns the interrupt status as false and re enters the spin:

final boolean acquireQueued(final Node node, int arg) {
    // Records whether the current node is cancelled. The default value is true, indicating cancellation
    boolean failed = true;
    try {
        // Mark whether the current node is interrupted. The default value is false, indicating that the current node is not interrupted
        boolean interrupted = false
        // spin
        for (;;) {
            // Gets the previous node of the current node
            final Node p = node.predecessor();
            // If the current node is the head node, it means that the current node is about to wake up and try to preempt resources
            if (p == head && tryAcquire(arg)) {
                // Set the current node as the head node, empty the previous node of the current node, and unbind the current node from the current thread
                setHead(node);
                // Empty the next node of the original header node to facilitate GC recycling
                p.next = null; // help GC
                // Mark the current node as false, indicating that it has not been cancelled
                failed = false;
                // Returns false, indicating that the current node is not interrupted
                return interrupted;
            }
            // No matter whether the current node is the head node or not, the execution here means that the acquisition of resources fails. The front node is processed and the current node is blocked
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // Marked as true indicates that the current node is interrupted
                interrupted = true;
        }
    } finally {
        // If the current node is cancelled, cancel the operation
        if (failed)
            cancelAcquire(node);
    }
}

Because the previous node of thread B is the head node, enter the tryAcquire() method to preempt resources. If the preemption succeeds, return true, set the current node as the head node, and unbind with thread B.

5.5.2 unlocking resources

When thread B finishes executing, it calls the unlock() method to release resources and wake up the thread.

The next node of the head node is the node where thread C is located, and thread C is awakened.

5.6 thread C executes and ends

5.6.1 preemption of resources

After thread C is released in the parkAndCheckInterrupt() method, it returns the interrupt status as false and re enters the spin.

Because the previous node of thread C is the head node, enter the tryAcquire() method to preempt resources. If the preemption succeeds, return true, set the current node as the head node, and unbind the same thread C.

5.6.2 unlocking resources

After the execution of thread C, call the unlock() method to release resources and wake up the thread.

The next node of the head node is empty and no thread will be awakened.

6 fair lock and unfair lock

6.1 unfair lock

When a thread with a non fair lock obtains resources, it will try to obtain resources. If it succeeds, it will occupy resources immediately. If it fails, it will try to occupy resources.

When resources are available, it will not judge whether there are threads waiting in the current queue, that is, the newly added thread can compete for resources with the awakened thread.

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
...
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
...
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
...
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

6.2 fair lock

When a fair lock thread obtains resources, it does not attempt to obtain resources, but attempts to occupy resources.

When resources are available, it will judge whether there are threads waiting in the current queue. The newly added thread cannot compete for resources with the awakened thread.

final void lock() {
    acquire(1);
}
...
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
...
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;
}

6.3 analyzing the hasQueuedPredecessors() method

Comparing the implementation of tryAcquire() method in NonfairSync and FairSync, it is found that there is one more judgment in tryAcquire() method in FairSync:

!hasQueuedPredecessors()

The hasQueuedPredecessors() method is used to judge whether there are valid nodes in the waiting queue when a fair lock is locked. This method is used to judge whether there are other nodes queued before the current node:

If false is returned, it means that there is no. If it is reversed, it means that the current node does not need to queue and needs to continue to perform resource consuming operations.

If true is returned, it means yes. If negative is false, it means that the current node needs to queue and join the waiting queue.

View the hasQueuedPredecessors() method defined in AQS:

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

Judge whether h is not equal to T. If it is not true, it indicates that h is equal to t, that the head node and tail node are the same, that the current queue is uninitialized (both head node and tail node are empty nodes) or that the current queue has only one node (both head node and tail node are empty nodes), that there is no need to queue, return false, take the opposite as true, and try to occupy resources.

Judge whether h is not equal to T. If it is true, it indicates that h is not equal to t, indicating that there are two different nodes. Continue to judge whether the next node of the head node is empty. If it is true, it means that the next node is empty. Maybe the previous thread is executing the initialization enq() method, just initialized the head node through the CAS operation compareAndSetHead(), and has not assigned a value to the tail node. At this time, the head node is not empty, the tail node is empty, and the next node of the head node is empty. Return true, If it is negative, it is false and needs to be queued.

Judge whether h is not equal to T. If it is true, it indicates that h is not equal to t, indicating that there are two different nodes. Continue to judge whether the next node of the head node is empty. If it is not true, it means that the next node is not empty. Continue to judge whether the thread encapsulated by the next node is not equal to the current thread. If it is true, it indicates that the next thread is not the current thread and returns true. If it is reversed, it is false and needs to be queued.

Judge whether h is not equal to T. If it is true, it indicates that h is not equal to t, indicating that there are two different nodes. Continue to judge whether the next node of the head node is empty. If it is not true, it means that the next node is not empty. Continue to judge whether the thread encapsulated by the next node is not equal to the current thread. If it is not true, it indicates that the next thread is the current thread, and returns false. If it is reversed, it is true, trying to occupy resources.

7 custom synchronizer

7.1 implementation method

Different user-defined synchronizers compete for shared resources in different ways. When implementing the user-defined synchronizer, you only need to obtain and release the shared resource state. As for the maintenance of the specific thread waiting queue (such as failed to obtain resources, entering the queue and waking up out of the queue), AQS has been implemented at the bottom.

The following methods are mainly implemented when customizing the synchronizer:

Ishldexclusively(): whether the thread is monopolizing resources. You only need to implement condition.

tryAcquire(int): exclusive mode. When trying to obtain resources, it returns true if successful, and false if failed.

tryRelease(int): exclusive mode. When attempting to release resources, it returns true if successful, and false if failed.

Tryacquiresered (int): sharing mode. Try to get the resource. A negative number indicates failure; 0 indicates success, but there are no available resources left; A positive number indicates success with resources remaining.

Tryrereleaseshared (int): sharing mode. Try to release the resource. If wake-up is allowed after release, the subsequent waiting nodes will return true; otherwise, return false.

Generally speaking, custom synchronizers are either exclusive or shared. They only need to implement one of tryacquire tryrelease and tryacquiresered tryrereleaseshared. However, AQS also supports user-defined synchronizers to realize exclusive and sharing at the same time, such as ReentrantReadWriteLock.

7.2 examples

7.2.1 ReentrantLock

Take ReentrantLock as an example. state is initialized to 0, indicating that it is not locked.

When thread A calls the lock() method to obtain resources, it will call tryAcquire() to occupy resources and increase the value of state by 1.

After that, other threads will fail in tryAcquire(). Until thread A calls the unlock() method to release resources and reduces the value of state by 0, other threads will not have A chance to obtain the lock.

Of course, before releasing the lock, thread A can acquire the lock repeatedly (the state will accumulate), which is the concept of reentrant. However, it should be noted that it is necessary to release as many times as it is obtained, so as to ensure that the state can return to the zero state.

7.2.2 CountDownLatch

Taking CountDownLatch as an example, the task is divided into n sub threads to execute, and the state is initialized to n (n is consistent with the number of threads). The N sub threads are executed in parallel. After each sub thread is executed, it will call the countDown() method once, and use the CAS operation to reduce the state value by 1. When all child threads are executed, the value of state changes to 0. At this time, the unpark() method will be called to wake up the main thread, and then the main thread will wake up from the await() method to continue the remaining actions.

Posted by schoolmommy on Wed, 13 Oct 2021 08:35:40 -0700