Detailed explanation of AQS of java concurrency

What is AQS

In the jdk we usually use, there is such a package java.util.current, which is a concurrent toolkit, which makes our concurrent programming easy.
Among them, there are many familiar tools, such as ReentrantLock, Semaphore, CountDownLatch, CycliBarrier, etc. They are all concurrent tools, and they also belong to the lock category in java.
When we use them happily (maybe unhappy), have we ever thought about such a question: how do they achieve it?

So, the protagonist of our article, AQS, has appeared, which is the core or foundation for building the above tools.

The full name of AQS is (AbstractQueuedSynchronizer), and this class is under the java.util.concurrent.locks package.
You can call it a synchronizer.

  • It is used to build the basic framework of locks or other synchronization components
  • The tools mentioned above are all implemented through AQS
  • We can also use AQS to customize our own synchronization components (for example, write a mutual exclusion lock, twins lock, etc.)
  • The role of AQS is to encapsulate the underlying mechanism of how to compete for resources, and provide a simple method for programmers to achieve resource acquisition; at the same time, provide a mechanism for thread blocking, waiting in line, and waking up competition when acquiring shared resources fails. These failed threads. Therefore, after using AQS, we don't have to worry about how to achieve resource acquisition when we define synchronization components. If we establish a synchronization queue queuing mechanism, we can focus on how to implement the logic of custom components.
  • The core idea of ​​AQS is: if the requested shared resource is free, the thread currently requesting the resource is set as a valid worker thread, and the shared resource is set to a locked state. If the requested shared resource is occupied, then a mechanism for thread blocking and waiting and lock allocation when awakened is required. This mechanism AQS is implemented with CLH queue locks, that is, threads that cannot obtain locks temporarily are added to the queue.

Composition of AQS

It is easier to understand AQS in two parts.

The first part is AQS 's management of synchronization status :

  • AQS uses an int member variable to represent the synchronization state
  • AQS provides protected types of getState, setState, compareAndSetState three methods to help us operate on the synchronization state, the meaning of these methods is known. Through these three methods, we can get the synchronization status, set the synchronization status, and modify the synchronization status of the CAS. (The core of AQS synchronization state management)
  • At the same time, AQS requires us to rewrite certain methods, as shown below:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
  • In this case, the methods that need to be rewritten are divided into two categories, exclusive and shared, which will be discussed later. In short, these two categories must be implemented at least one category, or both
  • In the end, when using AQS, we did not use our own rewrite method, but the template method provided by the synchronizer (the template method can be understood as the overall logic has been determined, and the specific small implementation method is just defined by ourselves. For example: We have a template method whose purpose is to eat. The logical flow is to open the refrigerator-take out the ingredients-cook-eat. The whole process is fixed, but what kind of ingredients are taken out and what kind of dishes are prepared , Is our own rewrite method)
  • Use such a schematic diagram to understand: the user uses the three management synchronization state methods provided by AQS, rewrites some methods, and finally uses the template method
    Insert picture description here
  • After this step, we can realize our own ideas based on the AQS mechanism.

The second part is about how to perform thread blocking, thread queuing, and thread wake-up operations when AQS fails to obtain synchronization status.
The synchronous queue is used here .
Insert picture description here
As shown in the figure: when the thread fails to obtain the synchronization status, the synchronizer will construct a node of the current thread and the waiting status information and add it to the synchronization queue.

  • This is actually a CLH lock
  • In the synchronizer, for this queue, only the head node and the tail node are maintained.
  • The node currently acquiring the synchronization status is the head node.
  • When a new node is added to the synchronization queue, thread safety needs to be considered, so there is a compareAndSetTail method to ensure (if it is not successfully enqueued at the time, it will always spin and try to enqueue).
  • The queue is obviously FIFO
  • Non-head node, it will enter the blocking state later
  • When the head node releases the synchronization state, it will wake up its successor nodes
  • Regarding the node in the synchronization queue, it has a predecessor and a successor. At the same time, it has two types: exclusive and shared. One of the key attributes is:
    Insert picture description here
  • Here is mainly signal, it is very important, the current node

Use of AQS

The main use of AQS is inheritance. The subclass manages the synchronization state by inheriting AQS and rewriting the methods specified by it. During the rewriting process, it is inevitable to modify the synchronization state. At this time, you need to use the three methods provided by AQS. To perform the operation (described above, not to repeat).
The subclass is recommended to be defined as a static inner class of a custom synchronization component.
Specifically, we can look at an example:
Insert picture description here
this is a schematic diagram of the structure of Semaphore in a concurrent package. It integrates a (defined) static internal class. Here, because fair locks and unfair locks are to be implemented, an abstraction is introduced. Sync, no matter, anyway, he is integrated in the custom component, and then this class inherits our AQS.
When the custom component specifically implements the function, it can be called in the form of Sync.acquire or release.

Realization of two resource sharing methods

AQS has two sharing methods for shared resources

  • Exclusive: Only one thread holds the synchronization state at the same time. Implementations include: ReentrantLock, etc.
  • Shared: There can be multiple (generally upper limit) threads holding the synchronization state at the same time. Implementations are: Semaphore, CountDownLatch, CycliBarrier

(Of course there are synchronization components, which use both, such as read-write locks, which are read-shared and write-only)

Here we emphasize that only threads that fail to obtain the synchronization status will create nodes and enter the synchronization queue.

Exclusive resource acquisition and release:
Let's look directly at the template method here.
The first is the acquire method. Obtain

public final void acquire(int arg) {
    
    
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • This method first calls the tryAcquire method (the method we rewritten) to try to obtain the synchronization status, if it fails, go to the next step
  • First call the addWait method, which is to create the corresponding node for the current thread, and then add it to the synchronization queue. The specific logic here is to quickly add at the end first, if the addition fails (CAS fails or the end node is empty), it enters the loop and continuously tries to add CAS. Here, CAS is used to ensure thread safety.
  • Here we can see that if the queue is empty at this time, a virtual node will be created . The meaning of the existence of virtual nodes will be discussed below. The significance of the existence of virtual nodes here is to ensure that each real node has a
 	//将节点加入同步等待队列,mode为Node.EXCLUSIVE
	private Node addWaiter(Node mode) {
    
    
	    //封装当前线程的Node节点对象
	     Node node = new Node(Thread.currentThread(), mode);
	     // 获取尾节点
	     Node pred = tail;
	    //如果尾节点不为空
	     if (pred != null) {
    
    
	         //将当前线程的前驱指针指向尾节点
	         node.prev = pred;
	         //cas方式将当前线程的节点对象设置为尾节点,如果成功,则将pred节点的后继节点指向当前线程节点
	         if (compareAndSetTail(pred, node)) {
    
    
	             pred.next = node;
	             return node;
	         }
	     }
	    //如果尾节点为空,或者修改尾节点失败,则执行enq方法
	     enq(node);
	     return node;
	 }
	/**
	* 此方法采用死循环的方式确保成功将当前线程的节点添加至队列尾部
	*/
	  private Node enq(final Node node) {
    
    
	      for (;;) {
    
    
	          //获取尾节点
	          Node t = tail;
	          //如果尾节点为空,则初始化
	          if (t == null) {
    
     // Must initialize
	              //新建一个空节点,并将tail和head都指向这个节点
	              if (compareAndSetHead(new Node()))
	                  tail = head;
	          } else {
    
    
	              //如果尾节点不为空,则再次尝试将当前线程的节点添加至队列尾部
	              node.prev = t;
	              if (compareAndSetTail(t, node)) {
    
    
	                  t.next = node;
	                  return t;
	              }
	          }
	      }
  • Then call the acquireQueued method, which is an endless loop. The logic is to first determine whether the predecessor node of the current node is the head node, and if it is, try to obtain the synchronization state. Then the next step is to call the method provided by LockSupport to block the current thread and wait for the head node to wake up. The source code is as follows
final boolean acquireQueued(final Node node, int arg) {
    
    
    //failed默认为true
    boolean failed = true;
    try {
    
    
        //中断标志默认为false
        boolean interrupted = false;
        //死循环
        for (;;) {
    
    
            //获取当前线程节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点,则再次尝试获取锁,如果成功,则将当前节点设置为头节点
            if (p == head && tryAcquire(arg)) {
    
    
                //设置头节点
                setHead(node);
                //将前驱节点的next指针置为null,方便gc回收
                p.next = null; // help GC
                //修改failed标识为false
                failed = false;
                //返回中断标志false
                return interrupted;
            }
            //如果前驱节点不是头节点或者获取锁失败,则判断是否需要挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    
    
        //如果获取状态失败,则取消同步器状态的获取,什么情况下会失败,可能只有异常的情况
        if (failed)
            cancelAcquire(node);
    }
}
  • Here, if the addition fails,
    let's briefly summarize: the current thread first tries to obtain the synchronization status, if it fails, it creates a node and tries to enter the synchronization queue.
    After entering, judge whether your predecessor node is the head node, and if it is, try to obtain it. If the previous step fails, it blocks itself and waits for the head node to wake up. (Before blocking, I tried to get twice)

The blocking here is the park method of LockSupport called, which will cause the thread to block.
There are three types of threads being awakened: other threads call unpark, the thread is interrupted, and other abnormal situations.

Regarding the synchronization state here, for custom synchronization components, the synchronization state needs to be set by ourselves. You can use setState to set it in the constructor. For the logic here, we can define the synchronization state variable to be 0 without lock and 1 with lock, so in When performing CAS, expect=0, update=1 to get the synchronization status.

Then release :

	public final boolean release(int arg) {
    
    
	    //如果释放锁成功
	    if (tryRelease(arg)) {
    
    
	        //获取头节点
	        Node h = head;
	        //如果头节点不为空且头节点的状态不等于0
	        if (h != null && h.waitStatus != 0)
	            //唤醒后继节点线程
	            unparkSuccessor(h);
	        return true;
	    }
	    return false;
	}

The head node is a thread that holds the synchronization state. When the synchronization state is released, it will wake up its successor node and let it compete for the synchronization state. (If it is a fair lock, it must be the successor node of the head node, but if it is an unfair lock, it may be another new thread)

Shared resource acquisition and release:
also look at the source code acquisition of the template method

	public final void acquireShared(int arg) {
    
    
	   if (tryAcquireShared(arg) < 0)
	       doAcquireShared(arg);
	}
	private void doAcquireShared(int arg) {
    
    
	   final Node node = addWaiter(Node.SHARED);//指明节点类型是共享式的
	   boolean failed = true;
	   try {
    
    
	       boolean interrupted = false;
	       for (;;) {
    
    
	           final Node p = node.predecessor();
	           if (p == head) {
    
    
	               int r = tryAcquireShared(arg);
	               if (r >= 0) {
    
    
	                   setHeadAndPropagate(node, r);
	                   p.next = null; // help GC
	                   if (interrupted)
	                       selfInterrupt();
	                   failed = false;
	                   return;
	              }
	          }
	           if (shouldParkAfterFailedAcquire(p, node) &&
	               parkAndCheckInterrupt())
	               interrupted = true;
	      }
	  } finally {
    
    
	       if (failed)
	           cancelAcquire(node);
	  }
	}
  • Because it is shared, it is not the same as the previous exclusive synchronization state variable. For example, if we set state to N, when a thread acquires the synchronization state, it performs a decrement operation. If it is 0, it means that the current thread holding the synchronization state has reached the maximum value, and other threads need to be as in the exclusive type. , Enter the queue and wait in line.
  • Analyze the code above.
  • The first is the template method acquireShared method, which first calls our rewritten tryAcquireShared method to try to acquire, if the return value is greater than or equal to 0, it means success, otherwise it fails, go to the next step
  • Execute the doAcquireShared method, here is also an infinite loop, when the current driver node is the head node, try to obtain the synchronization status, if it fails, block yourself and wait for wake-up

The difference between exclusive acquisition and shared acquisition Let
me emphasize that there is no difference between the whole and the exclusive type here. There is only one very important setHeadAndPropagate(node, r);method. We can turn to the above and find that in the exclusive type, setHead(node);
both are used. What is the difference:
Look at the source code of the former:

//设置队列的头节点,传入的参数为node和允许获取同步器状态的值
private void setHeadAndPropagate(Node node, int propagate) {
    
    
     //获取头节点
        Node h = head; // Record old head for check below
     //将当前线程设置为头节点
        setHead(node);        
     /**
     * 这里我们主要关注释放同步器状态的条件:
     * 1)propagate>0 表示允许获取同步器状态
     * 2)刚才拿到的head节点为null或者head节点的状态小于0即非CANCLLED状态
     * 3)再次去head节点,如果head节点为null或者head节点的状态小于0即非CANCLLED状态
     */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
    
    
            //获取当前节点的后继节点
            Node s = node.next;
            //后继节点为空或者后继节点是共享的
            if (s == null || s.isShared())
                //共享模式下修改头节点状态唤醒后继线程
                doReleaseShared();
        }
    }
//判断是否共享模式
 final boolean isShared() {
    
    
     return nextWaiter == SHARED;

The only difference is that the synchronization state is acquired in exclusive mode, but the current thread node is set as the head node.
On this basis, the shared mode wakes up subsequent non-exclusive mode nodes.
(Exclusive only wakes up when released)
There is a wake-up operation, so the method is also named Propagate, which passes the synchronization state.

Here we take a look at the source code of the doReleaseShared method (I can’t write it here, I’ll stop here for the time being)
release

	 //共享模式下释放同步器状态
	public final boolean releaseShared(int arg) {
    
    
	    //是否允许在共享模式下释放同步器状态,如果释放成功则返回true,失败返回false
	     if (tryReleaseShared(arg)) {
    
    
	         //如果释放同步器状态成功,则修改头节点,唤醒后继节点
	         doReleaseShared();
	         return true;
	     }
	     return false;
 }

The difference here with the exclusive type is that the tryRelease method here must ensure the safe release of the synchronization state, which means that the shared type has multiple threads holding synchronization resources.

(Let's open an article for the AQS related synchronization components later, it's a bit too much)

In terms of sharing, if you actually look at the source code, it seems that if you are in the synchronization queue, only two nodes can obtain the synchronization state. The new thread should be able to directly obtain resources without competing in the queue, so this is one An unfair mechanism. So there is a fair/unfair model in Semaphore. Fairness requires an additional judgment on whether there are nodes in the queue.

Reference

AQS shared mode
AQS exclusive mode
JavaGuide
concurrent programming Semaphore source code analysis

Guess you like

Origin blog.csdn.net/qq_34687559/article/details/114240245