A text with your depth understanding of Java in the AQS!

01 AQS Overview

AbstractQueuedSynchronizer abstract queue synchronizer referred AQS, which is the basis of component implementation synchronizer, juc achieve the following concurrent Lock as well as some tools is achieved through AQS, through AQS here we look at the class diagram about, let's summarize the principle of AQS. Take a look at the class diagram of AQS.


1


(1) AQS is a built-in FIFO to complete the work queue threads of two-way queue (internal nodes by recording head and the tail end of the first team and the team element, the element node types Node type, later we will see the Node specific configuration).

/ * Wait queue head of the queue node (lazy loading, embodied here as a case of a failure of competition, join the queue thread synchronization method of execution to enq time will create a Head node). The node can only be modified setHead method. And waitStatus node can not * CANCELED /  
Private the Node transient volatile head;  
/ ** queue tail node, is lazy loading. (Enq method). Only modify * / with the addition of a new node blocking  
private transient volatile Node tail;

(2) wherein the Node in the thread queue for storing threads into the AQS reference node Node inside thread SHARED flag is represented as a shared resource acquisition failure is added to the blocked queue; Node thread as represented in obtaining EXCLUSIVE exclusive resources failed to be added to the queue blocked. waitStatus represents the state of the current thread waiting:

① CANCELLED = 1: indicates that the thread because the interrupt or wait for the timeout, from the need to wait in the queue wait canceled;

② SIGNAL = -1: thread1 current thread lock occupied, queue head (merely representative of the first node, which is not stored thread reference) node1 successor node in a wait state, if the thread has possession of the lock or lock release thread1 CANCEL and then we will notify the node node1 to acquire the lock execution.

③ CONDITION = -2: represents the nodes in the waiting queue (in this case is waiting on a lock condition, the following principles will be written on Condition), when the thread holding the lock calls Condition of signal () after the method, the node will transfer from the wait queue to the condition of the lock of the synchronous queue, to compete lock. (Note: This synchronization queue is what we mean AQS maintenance FIFO queue, waiting queue is a queue associated with each condition)

④ PROPAGTE = -3: obtaining a shared state will be transferred to the successor node acquires the shared state represents the synchronization.


(3) AQS maintained a single status information that volatile state (by the AQS Unsafe related method, in order to obtain the atomic this manner by the thread state). AQS provides getState (), setState (), compareAndSetState () function modifies the value (actually calls compareAndSwapInt method of unsafe). Here are some of the member variable AQS and the renewal of state

// This is what we just said head node, lazy loading (only need to build competitive failed synchronization queue time, will create the head), if the head node exists, it can not be waitStatus CANCELED  
Private transient volatile the Node head ;  
// current synchronization queue tail node reference, is lazy loaded, only when calls enq method will add the node a new the wait  
Private transient volatile the node tail;  
// AQS core: synchronization status  
Private volatile int state;  
protected int Final getState () {      
          return State;  
protected void the setState Final (newState The int) {      
          State = newState The;  
protected Final Boolean compareAndSetState (Expect int, int Update) {      
          return unsafe.compareAndSwapInt (the this, stateOffset, Expect, Update);  
}

(4) AQS-based designer template method pattern. When using synchronous need to inherit and override the specified method, and the subclass generally recommended as defined synchronization component static inner classes, subclasses override these methods later used for AQS work is a template method provided in these templates subclass override the calling method. Wherein subclasses can override the method:

// synchronization state acquiring exclusive, implementation of the method need, and determines whether the current state of the synchronization state as expected, and then to set up synchronization status CAS  
protected Boolean to tryAcquire (int Arg) {  
          the throw an UnsupportedOperationException new new (); 
// Exclusive the synchronization state is released, threads waiting to acquire synchronization state may have access to the synchronization status  
protected Boolean tryRelease (int Arg) {  
          the throw an UnsupportedOperationException new new (); 
// shared acquiring synchronization state  
protected tryAcquireShared int (int Arg) {  
          the throw new new an UnsupportedOperationException (); 
// try to release the state to the synchronization state in shared mode. This method is always invoked by the thread to perform release.  
int tryReleaseShared protected (int Arg) {  
          the throw an UnsupportedOperationException new new ();  
// whether the current thread holds a synchronizer in exclusive mode, the process generally indicates whether the current thread monopolized  
protected int isHeldExclusively (int arg) { 
          throw new UnsupportedOperationException();
}

(. 5) AQS inner class ConditionObject is achieved by binding thread synchronization locking, AQS ConditionObject access variables (state, queue) directly, ConditionObject a condition variable corresponding to each ConditionObject await a call queue for storing threads have a condition variable after the method is blocked thread.

02 AQS in exclusive mode

Above we simply understand a bit AQS basic components, here by ReentrantLock of unfair lock to lock and release the lock a specific analysis of the process of AQS exclusive mode of implementation.

1. Non-locking lock fair process

Simply put, AQS all requests will constitute a thread CLH queue, when a thread is finished (lock.unlock ()) will activate its successor node, but not the executing thread in the queue, and those awaiting execution all thread is blocked (park ()). As shown below.


1


(1) assuming that at this time of the initial situation, not how competitive this task to request state, at this time if the first thread thread1 calls the lock method to request a lock, will first by CAS way state is updated to 1, indicating thread1 get yourself a lock and an exclusive lock thread holder to thread1.

final void lock() {     
      if (compareAndSetState(0, 1))         
      //setExclusiveOwnerThread是AbstractOwnableSynchronizer的方法,AQS继承了AbstractOwnableSynchronizer         
      setExclusiveOwnerThread(Thread.currentThread());     
      else         
      acquire(1); 
}

(2)这个时候有另一个线程thread2来尝试或者锁,同样也调用lock方法,尝试通过CAS的方式将state更新为1,但是由于之前已经有线程持有了state,所以thread2这一步CAS失败(前面的thread1已经获取state并且没有释放),就会调用acquire(1)方法(该方法是AQS提供的模板方法,它会调用子类的tryAcquire方法)。非公平锁的实现中,AQS的模板方法acquire(1)就会调用NofairSync的tryAcquire方法,而tryAcquire方法又调用的Sync的nonfairTryAcquire方法,所以我们看看nonfairTryAcquire的流程。

//NofairSync 
protected final boolean tryAcquire(int acquires) {     
          return nonfairTryAcquire(acquires); 
} 
final boolean nonfairTryAcquire(int acquires) {     
      //(1)获取当前线程     
      final Thread current = Thread.currentThread();     
      //(2)获得当前同步状态state     
      int c = getState();     
      //(3)如果state==0,表示没有线程获取     
      if (c == 0) {         
          //(3-1)那么就尝试以CAS的方式更新state的值         
          if (compareAndSetState(0, acquires)) {             
                //(3-2)如果更新成功,就设置当前独占模式下同步状态的持有者为当前线程             
                setExclusiveOwnerThread(current);             
                //(3-3)获得成功之后,返回true             
                return true;         
          }     
      }     
      //(4)这里是重入锁的逻辑     
     else if (current == getExclusiveOwnerThread()) {         
          //(4-1)判断当前占有state的线程就是当前来再次获取state的线程之后,就计算重入后的state         
          int nextc = c + acquires;         
         //(4-2)这里是风险处理         
         if (nextc < 0) 
             // overflow             
             throw new Error("Maximum lock count exceeded");         
            //(4-3)通过setState无条件的设置state的值,(因为这里也只有一个线程操作state的值,即         
            //已经获取到的线程,所以没有进行CAS操作)         
            setState(nextc);         
            return true;     
        }      
       //(5)没有获得state,也不是重入,就返回false     
       return false; 
}

总结来说就是:

  • 获取当前将要去获取锁的线程thread2。

  • 获取当前AQS的state的值。如果此时state的值是0,那么我们就通过CAS操作获取锁,然后设置AQS的线程占有者为thread2。很明显,在当前的这个执行情况下,state的值是1不是0,因为我们的thread1还没有释放锁。所以CAS失败,后面第3步的重入逻辑也不会进行

  • 如果当前将要去获取锁的线程等于此时AQS的exclusiveOwnerThread的线程,则此时将state的值加1,这是重入锁的实现方式。

  • 最终thread2执行到这里会返回false。

(3)上面的thread2加锁失败,返回false。那么根据开始我们讲到的AQS概述就应该将thread2构造为一个Node结点加入同步队列中。因为NofairSync的tryAcquire方法是由AQS的模板方法acquire()来调用的,那么我们看看该方法的源码以及执行流程。

//(1)tryAcquire,这里thread2执行返回了false,那么就会执行addWaiter将当前线程构造为一个结点加入同步队列中 
public final void acquire(int arg) {     
       if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))         
          selfInterrupt(); 
}

那么我们就看一下addWaiter方法的执行流程。

private Node addWaiter(Node mode) {     
       //(1)将当前线程以及阻塞原因(是因为SHARED模式获取state失败还是EXCLUSIVE获取失败)构造为Node结点     
       Node node = new Node(Thread.currentThread(), mode);     
       //(2)这一步是快速将当前线程插入队列尾部     
       Node pred = tail;     
       if (pred != null) {         
          //(2-1)将构造后的node结点的前驱结点设置为tail         
          node.prev = pred;         
          //(2-2)以CAS的方式设置当前的node结点为tail结点         
          if (compareAndSetTail(pred, node)) {             
             //(2-3)CAS设置成功,就将原来的tail的next结点设置为当前的node结点。这样这个双向队             
             //列就更新完成了            
             pred.next = node;             
             return node;         
          }     
        }     
        //(3)执行到这里,说明要么当前队列为null,要么存在多个线程竞争失败都去将自己设置为tail结点,     
        //那么就会有线程在上面(2-2)的CAS设置中失败,就会到这里调用enq方法     
        enq(node);     
        return node; 
}

那么总结一下add Waiter方法

  • 将当前将要去获取锁的线程也就是thread2和独占模式封装为一个node对象。

  • 尝试快速的将当前线程构造的node结点添加作为tail结点(这里就是直接获取当前tail,然后将node的前驱结点设置为tail),并且以CAS的方式将node设置为tail结点(CAS成功后将原tail的next设置为node,然后这个队列更新成功)。

  • 如果2设置失败,就进入enq方法。

在刚刚的thread1和thread2的环境下,开始时候线程阻塞队列是空的(因为thread1获取了锁,thread2也是刚刚来请求锁,所以线程阻塞队列里面是空的)。很明显,这个时候队列的尾部tail节点也是null,那么将直接进入到enq方法。所以我们看看enq方法的实现

private Node enq(final Node node) {     
        for (;;) {         
            //(4)还是先获取当前队列的tail结点         
            Node t = tail;         
            //(5)如果tail为null,表示当前同步队列为null,就必须初始化这个同步队列的head和tail(建立一个哨兵结点)         
            if (t == null) {              
            //(5-1)初始情况下,多个线程竞争失败,在检查的时候都发现没有哨兵结点,所以需要CAS的             
            //设置哨兵结点             
            if (compareAndSetHead(new Node()))                 
            tail = head;         
         }          
         //(6)tail不为null         
         else {             
                //(6-1)直接将当前结点的前驱结点设置为tail结点             
                node.prev = t;             
                //(6-2)前驱结点设置完毕之后,还需要以CAS的方式将自己设置为tail结点,如果设置失败,             
                //就会重新进入循环判断一遍             
                if (compareAndSetTail(t, node)) {                 
                    t.next = node;                 
                    return t;             
                }         
          }     
       } 
}

enq方法内部是一个自旋循环,第一次循环默认情况如下图所示

  • 首先代码块(4)处将t指向了tail,判断得到t==null,如图(1)所示;

  • 于是需要新建一个哨兵结点作为整个同步队列的头节点(代码块5-1处执行)

  • 完了之后如图(2)所示。这样第一次循环执行完毕。


1


第二次循环整体执行如下图所示。

  • 还是先获取当前tail结点然后将t指向tail结点。如下图的(3)

  • 然后判断得到当前t!=null,所以enq方法中进入代码块(6).

  • 在(6-1)代码块中将node的前驱结点设置为原来队列的tail结点,如下图的(4)所示。

  • 设置完前驱结点之后,代码块(6-2)会以CAS的方式将当前的node结点设置为tail结点,如果设置成功,就会是下图(5)所示。更新完tail结点之后,需要保证双向队列的,所以将原来的指向哨兵结点的t的next结点指向node结点,如下图(6)所示。最后返回。


1


总结来说,即使在多线程情况下,enq方法还是能够保证每个线程结点会被安全的添加到同步队列中,因为enq通过CAS方式将结点添加到同步队列之后才会返回,否则就会不断尝试添加(这样实际上就是在并发情况下,把向同步队列添加Node变得串行化了)

(4)在上面AQS的模板方法中,acquire()方法还有一步acquireQueued,这个方法的主要作用就是在同步队列中嗅探到自己的前驱结点,如果前驱结点是头节点的话就会尝试取获取同步状态,否则会先设置自己的waitStatus为-1,然后调用LockSupport的方法park自己。具体的实现如下面代码所示

final boolean acquireQueued(final Node node, int arg) {     
      boolean failed = true;     
      try {         
           boolean interrupted = false;         
           //在这样一个循环中尝试tryAcquire同步状态         
           for (;;) {             
                    //获取前驱结点             
                    final Node p = node.predecessor();             
                    //(1)如果前驱结点是头节点,就尝试取获取同步状态,这里的tryAcquire方法相当于还是调             
                    //用NofairSync的tryAcquire方法,在上面已经说过             
                    if (p == head && tryAcquire(arg)) {                 
                       //如果前驱结点是头节点并且tryAcquire返回true,那么就重新设置头节点为node                 
                       setHead(node);                 
                       p.next = null; //将原来的头节点的next设置为null,交由GC去回收它                 
                       failed = false;                 
                       return interrupted;             
                     }             
                     //(2)如果不是头节点,或者虽然前驱结点是头节点但是尝试获取同步状态失败就会将node结点             
                     //的waitStatus设置为-1(SIGNAL),并且park自己,等待前驱结点的唤醒。至于唤醒的细节             
                     //下面会说到             
                     if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())                 
                     interrupted = true;         
            }     
       } finally {         
                     if (failed)             
                     cancelAcquire(node);     
       } 
}

在上面的代码中我们可以看出,这个方法也是一个自旋循环,继续按照刚刚的thread1和thread2这个情况分析。在enq方法执行完之后,同步队列的情况大概如下所示。


1


当前的node结点的前驱结点为head,所以会调用tryAcquire()方法去获得同步状态。但是由于state被thread1占有,所以tryAcquire失败。这里就是执行acquireQueued方法的代码块(2)了。代码块(2)中首先调用了shouldParkAfterFailedAcquire方法,该方法会将同步队列中node结点的前驱结点的waitStatus为CANCELLED的线程移除,并将当前调用该方法的线程所属结点自己和他的前驱结点的waitStatus设置为-1(SIGNAL),然后返回。具体方法实现如下所示。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {     
        //(1)获取前驱结点的waitStatus     int ws = pred.waitStatus;     
        //(2)如果前驱结点的waitStatus为SINGNAL,就直接返回true     
        if (ws == Node.SIGNAL)         
        //前驱结点的状态为SIGNAL,那么该结点就能够安全的调用park方法阻塞自己了。         
        return true;     
        if (ws > 0) {         
                     //(3)这里就是将所有的前驱结点状态为CANCELLED的都移除         
                     do {             
                         node.prev = pred = pred.prev;         
                     } 
                     while (pred.waitStatus > 0);         
                         pred.next = node;     
        } else {         
                    //CAS操作将这个前驱节点设置成SIGHNAL。         
                    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);     
        }     
        return false; 
}

所以shouldParkAfterFailedAcquire方法执行完毕,现在的同步队列情况大概就是这样子,即哨兵结点的waitStatus值变为-1。


1


上面的执行完毕返回到acquireQueued方法的时候,在acquireQueued方法中就会进行第二次循环了,但是还是获取state失败,而当再次进入shouldParkAfterFailedAcquire方法的时候,当前结点node的前驱结点head的waitStatus已经为-1(SIGNAL)了,就会返回true,然后acquireQueued方法中就会接着执行parkAndCheckInterrupt将自己park阻塞挂起。

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

(5)我们梳理一下整个方法调用的流程,假设现在又有一个thread3线程竞争这个state,那么这个方法调用的流程是什么样的呢。

①首先肯定是调用ReentrantLock.lock()方法去尝试加锁;

②因为是非公平锁,所以就会转到调用NoFairSync.lock()方法;

③在NoFairSync.lock()方法中,会首先尝试设置state的值,因为已经被占有那么肯定就是失败的。这时候就会调用AQS的模板方法AQS.acquire(1)

④在AQS的模板方法acquire(1)中,实际首先会调用的是子类的tryAcquire()方法,而在非公平锁的实现中即Sync.nofairTryAcquire()方法。

⑤显然tryAcquire()会返回false,所以acquire()继续执行,即调用AQS.addWaiter(),就会将当前线程构造称为一个Node结点,初始状况下waitStatus为0。

⑥在addWaiter方法中,会首先尝试直接将构建的node结点以CAS的方式(存在多个线程尝试将自己设置为tail)设置为tail结点,如果设置成功就直接返回,失败的话就会进入一个自旋循环的过程。即调用enq()方法。最终保证自己成功被添加到同步队列中。

⑦加入同步队列之后,就需要将自己挂起或者嗅探自己的前驱结点是否为头结点以便尝试获取同步状态。即调用acquireQueued()方法。

⑧在这里thread3的前驱结点不是head结点,所以就直接调用shouldParkAfterFailedAcquire()方法,该方法首先会将刚刚的thread2线程结点中的waitStatue的值改变为-1(初始的时候是没有改变这个waitStatus的,每个新节点的添加就会改变前驱结点的waitStatus值)。

⑨thread2所在结点的waitStatus改变后,shouldParkAfterFailedAcquire方法会返回false。所以之后还会在acquireQueued中进行第二次循环。并再次调用shouldParkAfterFailedAcquire方法,然后返回true。最终调用parkAndCheckInterrupt()将自己挂起。

每个线程去竞争这个同步状态失败的话大概就会经历上面的这些过程。假设现在thread3经历上面这些过程之后也进入同步队列,那么整个同步队列大概就是下面这样了.


1


将上面的流程整理一下大概就是下面这个图


1


2. 非公平锁的释放流程

上面说一ReentrantLock为例到了怎样去获得非公平锁,那么thread1获取锁,执行完释放锁的流程是怎样的呢。首先肯定是在finally中调用ReentrantLock.unlock()方法,所以我们就从这个方法开始看起。

(1)从下面的unlock方法中我们可以看出,实际上是调用AQS的release()方法,其中传递的参数为1,表示每一次调用unlock方法都是释放所获得的一次state。重入的情况下会多次调用unlock方法,也保证了lock和unlock是成对的。

public void unlock() {     
       sync.release(1); 
       //这里ReentrantLock的unlock方法调用了AQS的release方法 
} 
public final boolean release(int arg) { 
       //这里调用了子类的tryRelease方法,即ReentrantLock的内部类Sync的tryRelease方法     
       if (tryRelease(arg)) {         
              Node h = head;         
              if (h != null && h.waitStatus != 0)             
              unparkSuccessor(h);         
              return true;     
       }     
       return false; 
}

(2)上面看到release方法首先会调用ReentrantLock的内部类Sync的tryRelease方法。而通过下面代码的分析,大概知道tryRelease做了这些事情。

①获取当前AQS的state,并减去1;

②判断当前线程是否等于AQS的exclusiveOwnerThread,如果不是,就抛IllegalMonitorStateException异常,这就保证了加锁和释放锁必须是同一个线程;

③如果(state-1)的结果不为0,说明锁被重入了,需要多次unlock,这也是lock和unlock成对的原因;

④如果(state-1)等于0,我们就将AQS的ExclusiveOwnerThread设置为null;

⑤如果上述操作成功了,也就是tryRelase方法返回了true;返回false表示需要多次unlock。

protected final boolean tryRelease(int releases) {     
          //(1)获取当前的state,然后减1,得到要更新的state     
          int c = getState() - releases;     
          //(2)判断当前调用的线程是不是持有锁的线程,如果不是抛出IllegalMonitorStateException     
          if (Thread.currentThread() != getExclusiveOwnerThread())         
          throw new IllegalMonitorStateException();     
          boolean free = false;     
          //(3)判断更新后的state是不是0     
          if (c == 0) {         
                       free = true;         
                       //(3-1)将当前锁持者设为null         
                       setExclusiveOwnerThread(null);     
          }     
          //(4)设置当前state=c=getState()-releases     
          setState(c);     
          //(5)只有state==0,才会返回true     
          return free; 
}

(3)那么当tryRelease返回true之后,就会执行release方法中if语句块中的内容。从上面我们看到,

if (tryRelease(arg)) {     
   //(1)获取当前队列的头节点head     
   Node h = head;     
   //(2)判断头节点不为null,并且头结点的waitStatus不为0(CACCELLED)     
   if (h != null && h.waitStatus != 0)         
   //(3-1)调用下面的方法唤醒同步队列head结点的后继结点中的线程         
   unparkSuccessor(h);     
   return true; 
}

(4)在获取锁的流程分析中,我们知道当前同步队列如下所示,所以判断得到head!=null并且head的waitStatus=-1。所以会执行unparkSuccessor方法,传递的参数为指向head的一个引用h.那下面我们就看看unparkSuccessor方法中处理了什么事情。


1


private void unparkSuccessor(Node node) {     
        //(1)获得node的waitStatus     
        int ws = node.waitStatus;     
        //(2)判断waitStatus是否小于0     
        if (ws < 0)         
        //(2-1)如果waitStatus小于0需要将其以CAS的方式设置为0         
        compareAndSetWaitStatus(node, ws, 0);     
        //(2)获得s的后继结点,这里即head的后继结点     
        Node s = node.next;     
        //(3)判断后继结点是否已经被移除,或者其waitStatus==CANCELLED     
        if (s == null || s.waitStatus > 0) {         
            //(3-1)如果s!=null,但是其waitStatus=CANCELLED需要将其设置为null         
            s = null;         
            //(3-2)会从尾部结点开始寻找,找到离head最近的不为null并且node.waitStatus的结点         
            for (Node t = tail; t != null && t != node; t = t.prev)             
            if (t.waitStatus <= 0)                 
            s = t;     
         }     
         //(4)node.next!=null或者找到的一个离head最近的结点不为null     
         if (s != null)         
         //(4-1)唤醒这个结点中的线程         
         LockSupport.unpark(s.thread); 
 }

从上面的代码实现中可以总结,unparkSuccessor主要做了两件事情:

①获取head节点的waitStatus,如果小于0,就通过CAS操作将head节点的waitStatus修改为0

②寻找head节点的下一个节点,如果这个节点的waitStatus小于0,就唤醒这个节点,否则遍历下去,找到第一个waitStatus<=0的节点,并唤醒。

(5)下面我们应该分析的是释放掉state之后,唤醒同步队列中的结点之后程序又是是怎样执行的。按照上面的同步队列示意图,那么下面会执行这些

thread1(获取到锁的线程)调用unlock方法之后,最终执行到unparkSuccessor方法会唤醒thread2结点。所以thread2被unpark

Recall, when thread2 in parkAndCheckInterrupt after calling acquireQueued method which is blocked pending the park, so after thread2 wakes continue acquireQueued methods for loop (here can look forward memories acquireQueued methods for What things do cycle);

for the cycle is the first thing to do in view of their predecessor node is not the first node (in the case of the above synchronous queue is satisfied);

predecessor node is the head node, it will call tryAcquire method attempts to acquire a state , because thread1 been released state, namely state = 0, so thread2 call tryAcquire method when it comes to CAS way to update the state from 0 to 1 is successful , so this time thread2 on access to the lock

Thread2 get state is successful, it will withdraw from acquireQueued method. Note that this time acquireQueued return value is false, it will withdraw from if conditions acquire the template method of AQS, the last execution of program code blocks themselves locked in.


Guess you like

Origin blog.51cto.com/14453419/2425163