Go deep into the AQS principles (I confused unfair locks and fair locks when I first started learning)

When it comes to concurrency, we have to say AQS(AbstractQueuedSynchronizer)that the so-called AQSabstract queue synchronizer has many lock-related methods defined internally. The well-known , ReentrantLock, ReentrantReadWriteLock, CountDownLatchetc. Semaphoreare all AQSimplemented based on .

AQSLet’s take a look at the relevant pictures first UML:

Below I have compiled a framework diagram of AQS for your reference.

 AQS implementation principle

AQSvolatile int stateOne (representing shared resources) and a FIFOthread waiting queue are maintained (this queue will be entered when multi-threads compete for resources and are blocked).

This volatilecan ensure visibility under multi-threading. When state=1it means that the current object lock has been occupied, other threads will fail when they try to lock. The thread that fails to lock will be put into a FIFOwaiting queue and will beUNSAFE.park() hung up by the operation . Starting, wait for other threads that acquired the lock to release the lock before they can be awakened.

Other stateoperations are done to CASensure the safety of concurrent modifications.

The specific principle can be briefly summarized with a picture.

 AQSProvides many implementation methods for locks.

  • getState(): Get the flag state value of the lock
  • setState(): Set the lock flag state value
  • tryAcquire(int): Acquire the lock exclusively. Try to obtain the resource, returning true if successful and false if failed.
  • tryRelease(int): Release the lock exclusively. Try to release the resource, returning true if successful and false if failed.

etc.

Directory Structure

Three threads (thread one, thread two, and thread three) lock/release the lock at the same time

The directory is as follows:

  • AQSImplemented internally when the thread locks successfully
  • AQSData model of waiting queue when thread 2/3 fails to lock
  • The implementation principle of thread one releasing the lock and thread two acquiring the lock
  • Explain the specific implementation principles of fair lock through thread scenarios
  • wait()Explain the a and signal()implementation principles in Condition through thread scenarios

Here we will draw pictures to analyze AQSthe internal data structure and implementation principle of each thread after locking and releasing the lock.

Scenario analysis

Thread 1 is locked successfully

If three threads concurrently seize the lock at the same time, thread one succeeds in seizing the lock, but thread two and three fail to seize the lock. The specific execution process is as follows:

The internal data at this time AQSis:

 Thread 2 and thread 3 failed to lock

 As can be seen from the picture, the nodes in the waiting queue Nodeare a two-way linked list. Here SIGNALis the attribute Nodein waitStatus, Nodeand there is another nextWaiterattribute in. This is not drawn in the picture. This Conditionwill be explained in detail later.

The ReentrantLock used here is an unfair lock . The thread comes in and directly CAStries to preempt the lock. If the preemption is successful, statethe value is changed to 1, and the object exclusive lock thread is set to the current thread.

Thread 2 failed to seize the lock

You can directly see the yellow markings for quick understanding

Let's analyze it according to the real scenario. After thread onestate successfully seizes the lock, it changes to 1. Thread two will inevitably fail by CASmodifying statethe variable. AQSAt this time FIFO, the data in the queue (First In First Out) is as shown in the figure:

Let’s dismantle the execution logic of thread 2 step by step:

java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire()

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

tryAcquire()Let’s take a look at the specific implementation first : : java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire()

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)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

nonfairTryAcquire()The value will be obtained first in the method state. If it is not 0, it means that the lock of the current object has been occupied by other threads. Then it is judged whether the thread occupying the lock is the current thread. If so, statethe value Implementation, accumulating statevalue , and decrementing statethe value sequentially when releasing the lock

If stateit is 0, perform CASthe operation and try to update statethe value to 1. If the update is successful, it means that the current thread is locked successfully.

Taking thread two as an example, because thread one has already statemodified it to 1, thread two will not succeed through CASthe modified statevalue. Lock failed.

Thread 2tryAcquire() will return false after execution , then execute addWaiter(Node.EXCLUSIVE)the logic and add itself to a FIFOwaiting queue. At this time, the pointer in the waiting pair tailis empty, and enq(node)the method is directly called to add the current thread to the end of the waiting queue. (This is a doubly linked list)

And the end method does this

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

tailThe pointer is empty during the first loop , enter the if logic, and use CASthe operation to set headthe pointer, which will headpoint to a newly created Nodenode. Data at this time AQS:

 After the execution is completed, head, tail, tall point to the first Nodeelement

Then execute the second loop and enter elsethe logic. At this time, there is already heada node. What needs to be done here is to hang the node corresponding to thread 2 behind the node. At this point there are two nodes in the queueNodeheadNode

 addWaiter()After the method is executed, the node information created by the current thread will be returned (note that because thread two is the first to join the waiting queue, it takes two cycles to create the second node). Continue to execute acquireQueued(addWaiter(Node.EXCLUSIVE), arg)the logic, and the parameter passed in at this time is the node information corresponding to thread two .Node

acquireQueued()This method will first determine whether the Nodecorresponding preceding node currently passed in is head, and if so, try to lock it. If the lock is successful, the current node will be set as headthe node, and the previous headnode will be left vacant to facilitate subsequent garbage collection .

If the lock fails or Nodethe preceding node is not heada node , the node will be changed to itshouldParkAfterFailedAcquire through a method , and finally the method will be executed to call and suspend the current thread .headwaitStatusSIGNAL=-1parkAndChecknIterruptLockSupport.park()

The data at this time AQSis as follows:

At this time , thread two is quietly staying AQSin the waiting queue, waiting for other threads to release the lock to wake it up.

Pay attention to the changes in the previous node each time

Thread three failed to seize the lock

After reading the analysis of the failure of thread two to seize the lock, it is very simple to analyze the failure of thread three to seize the lock.

tailAt this time, the node in the waiting queue points to thread two . ifAfter entering the logic, the node is redirected to thread threeCAS through instructions . Then thread three calls the method to perform the enqueue operation, which is consistent with the execution method of thread two above. After joining the queue, the corresponding value of thread two will be modified . Finally, thread three will also be suspended. At this time, the data in the waiting queue is as follows:tailenq()NodewaitStatus=SIGNAL

Thread releases the lock

Now let's analyze the process of releasing the lock. First, thread onehead releases the lock. After releasing the lock , the node's post-node will be awakened , which is our current thread two . The specific operation process is as follows:

 After execution, the waiting queue data is as follows:

At this time , thread two has been awakened and continues to try to acquire the lock. If it fails to acquire the lock, it will continue to be suspended. If the lock is acquired successfully, AQSthe data will be as shown in the figure.

 Thread releases the lock

先tryRelease()Method, this method is specifically implemented in ReentrantLock. If tryReleasethe execution is successful, continue to determine whether headthe node waitStatusis 0. We have seen before that the method headis waitStatue, SIGNAL(-1)and the method will be executed here unparkSuccessor()to wake up headthe post-node, which is the post-node in our figure above. The node corresponding to thread two .Node

ReentrantLock.tryRelease()After execution , stateit is set to 0, and the exclusive lock of the Lock object is set to null. Now look at AQSthe data below:

 Then execute java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()the method and wake up headthe post-node: here the main thing is to set headthe node waitStatusto 0.

headAt this time, point the pointer to the node corresponding to thread two again Node, and use LockSupport.unparkthe method to wake up thread two .

The awakened thread 2 will then try to acquire the lock and CASmodify statethe data with instructions. After the execution is completed, you can view AQSthe data:

At this time , thread two is awakened, and thread twopark continues execution where it was before , continuing to execute acquireQueued()the method. 

At this time , thread two is awakened and continues to execute forthe loop to determine whether the preceding node of thread twohead is . If so, continue to use tryAcquire()methods to try to acquire the lock. In fact, it is to use CASoperations to modify statethe value. If the modification is successful, it means that the lock acquisition is successful. Then set thread two as heada node, and then empty the previous headnode data. The empty node data is waiting to be garbage collected.

Thread two releases the lock/thread three adds the lock 

When thread two releases the lock, it will wake up the suspended thread three . The process is roughly the same as above. The awakened thread three will try to lock again.

Fair lock implementation principle

All the above locking scenarios are implemented based on unfair locks . Unfair locks are ReentrantLockthe default implementation. Let's take a look at the implementation principle of fair locks . Here we first use a picture to explain fair locks and unfair locks . The difference:

Unfair lock execution process:

Here we still use the previous thread model as an example. When thread two releases the lock, it wakes up the suspended thread three . The execution method of thread threetryAcquire() uses CASoperations to try to modify statethe value. If another thread comes at this time, thread four will also come. When performing a locking operation, the method will also be executed tryAcquire().

 In this case, competition will occur. If thread four succeeds in acquiring the lock, thread three still needs to stay in the waiting queue and be suspended. This is the so-called unfair lock . Thread three queued up hard to wait for itself to obtain the lock, but watched helplessly as thread four jumped in the queue and obtained the lock.

Fair lock execution process:

The difference between unfair locks and fair locks : The performance of unfair locks is higher than the performance of fair locks . Unfair locks can reduce CPUthe cost of waking up threads, and the overall throughput efficiency will be higher. CPUThere is no need to wake up all threads, which will reduce the number of awakened threads.

Although the performance of unfair locks is better than fair locks , there may be situations that lead to thread starvation . In the worst case, there may be a thread that never acquires the lock . However, compared with performance, the starvation problem can be temporarily ignored. This may be ReentrantLockone of the reasons why unfair locks are created by default.

Condition implementation principle

Should know

ConditionIt java 1.5only appeared in . It is used to replace the traditional method of realizingObject cooperation wait()between notify()threads. Compared with the method Objectof using . Therefore, it is generally recommended to usewait()notify()Conditionawait()signal()Condition

Among them , the methods in AbstractQueueSynchronizerare implemented , which are mainly provided and called externally.Conditionawaite(Object.wait())signal(Object.notify())

Condition Demo example

public class ReentrantLockDemo {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程一加锁成功");
                System.out.println("线程一执行await被挂起");
                condition.await();
                System.out.println("线程一被唤醒成功");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println("线程一释放锁成功");
            }
        }).start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程二加锁成功");
                condition.signal();
                System.out.println("线程二唤醒线程一");
            } finally {
                lock.unlock();
                System.out.println("线程二释放锁成功");
            }
        }).start();
    }
}

 Condition implementation principle diagram

 Thread one execution await()method:

await()The method first calls addConditionWaiter()to add the current thread to Conditionthe queue.

After execution, we can look at Conditionthe data in the queue:

 Here a node will be created using the current thread Node, waitStatusas CONDITION. Then the lock of the node will be released, and the previously parsed release()method will be called. After the lock is released, the suspended thread two will be awakened , and the thread two will continue to try to acquire the lock.

Then the calling isOnSyncQueue()method is to determine whether the current thread node is in the synchronization queue, because the lock has been released in the previous step, which means that a thread may have acquired the lock at this time and may have called the method. If it has been awakened, it should singal()not parkInstead, it exits whilethe method and continues to compete for the lock.

At this time , thread one is suspended, and thread two acquires the lock successfully. .............................

Advantages of condition

  • Condition can accurately control multiple different conditions. wait/notify can only be used with the synchronized keyword, and can only wake up one or all waiting queues;

  • Condition needs to be controlled using Lock. When using it, pay attention to unlock() in time after lock(). Condition has a mechanism similar to await, so deadlocks caused by locking methods will not occur. At the same time, the underlying implementation is park. /unpark mechanism, so it will not cause a deadlock of waking up first and then hanging. In a word, it will not cause a deadlock, but wait/notify will cause a deadlock of waking up first and then hanging.

Before understanding the reentrancy of ReentrantLock, let’s review how the synchronized keyword works.

synchronized is a keyword in Java that is used to implement thread synchronization and mutually exclusive access. When a thread enters a synchronized block or method, it will try to acquire the lock. If the lock has been acquired by another thread, the thread will be blocked until the lock is released. The thread that acquires the lock can execute the synchronized block or method and release the lock after completion.

Reentrancy (Reentrancy) means that the same thread can acquire the lock again without being blocked while holding the lock. In other words, a thread can repeatedly enter a block of code protected by a lock it already owns without being blocked by the lock it holds.

ReentrantLock is an implementation of a reentrant lock provided in Java. Unlike the synchronized keyword, ReentrantLock uses explicit locking and releasing locks to achieve thread synchronization. It allows a thread to repeatedly acquire the lock without being blocked while holding the lock. This is the embodiment of reentrancy.

For example, when thread A acquires the lock of ReentrantLock and enters the synchronized block, thread A can call the lock() method of ReentrantLock again inside the synchronized block to acquire the same lock. This is allowed because ReentrantLock will record the thread holding the lock and the number of times it is held. Only when thread A completely releases the lock it holds, other threads can obtain the lock.

The benefit of reentrancy is that it simplifies the programming model, allowing threads to repeatedly acquire and release locks in multiple levels of nested synchronization code without causing deadlocks or thread blocking. This makes ReentrantLock more flexible and controllable in some complex synchronization scenarios.

In summary, ReentrantLock's reentrancy allows the same thread to repeatedly acquire the same lock while holding the lock without being blocked. This mechanism simplifies the programming model and provides a more flexible and controllable thread synchronization mechanism.

This article refers to [In-depth AQS Principles] I drew 35 pictures just to let you get deeper into AQS - Nuggets

Guess you like

Origin blog.csdn.net/qq_52988841/article/details/132391686