AQS源码学习(一)---- ReentrantLock

一直对AQS认识的比较浅,工作之余希望开始整理一下对此处源码的阅读心得。

AQS整体理解

AQS:AbstractQueuedSyncrhonizer,抽象的基于队列的同步器

首先这是一个抽象类,其作用是作为一个同步器来给很多不同功能的 Lock 实现提供 对共享资源的 同步访问。

AQS内部使用 state 变量来保存共享资源的 数量。

例如 ReentrantLock中,其中lock()的实现,就是在内部先 实现了一个AQS的实现类Sync,调用了Sync的lock()方法, 而Sync的lock() 则是通过调用 AQS模板类中的 acquire()方法来操作 共享变量,从而实现锁的逻辑。

简单的理解state作为共享变量如何体现锁的概念?

对独占锁来说,可以理解为共享变量是一个全局唯一的,所以这时使用state=0来代表锁资源m还没被占用,state=1代表已经被占用;通过casState(0,1)来尝试加锁,casState(1,0)来尝试解锁

而不同的Lock实现,如ReentrantLock,Semaphore等,对于共享变量的设置都是不同的,有的是独占的,state最大为1,有的是资源池形式的,state可以等于n;

所以对于cas最终要acquire多少个资源,以及一些公平和非公平逻辑,这个操作和判断是留给Lock中 AQS的实现类Sync中定义的,也就是tryAcquire()方法,这个方法在acquire()会被调用,来完成对 state的操作;

而acquire()作为模板方法,用来实现等待队列的构建,以及线程的阻塞等

在这里插入图片描述

AQS的主要方法和业务目标

public final void acquire(int arg)
//负责调用tryAcquire(arg),获取失败的话,则进入等待队列
//此方法无法响应中断,如果出现中断,则返回中断标识,然后再重新调用interrupt()方法一次,来设置中断标识,避免AQS处理逻辑丢失了中断标识位,而恰好外部线程想判断中断标识位
  
public final void acquireInterruptibly(int arg)
//主要逻辑类似,只不过这个可以抛出中断异常

AQS基本框架

在这里插入图片描述

  • 上图中有颜色的为Method,无颜色的为Attribution
  • 总的来说,AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据。
  • 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。

原理概览

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改

在这里插入图片描述

数据结构

AQS中最基本的数据结构——NodeNode即为上面CLH变体队列中的节点

在这里插入图片描述
在这里插入图片描述

state

AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。

// java.util.concurrent.locks.AbstractQueuedSynchronizer

private volatile int state;

其中如果是第一次争抢state,是使用cas来争抢,如果是重入锁进行state设置,则直接set即可

这几个方法都是Final修饰的,说明子类中无法重写它们。我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)

独占模式加锁流程

在这里插入图片描述

共享模式加锁流程
在这里插入图片描述

对于我们自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就是AQS架构图中的第一层:API层。(通常是 重写 tryAcquire()方法,来修改当前 同步器对于获取state的一些自定义逻辑,比如是否公平,是否支持重入等

AQS重要方法与ReentrantLock的关联

从架构图中可以得知,AQS提供了大量用于自定义同步器实现的Protected方法。自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部):

方法名描述
protected boolean isHeldExclusively()该线程是否正在独占资源。只有用到Condition才需要去实现它。
protected boolean tryAcquire(int arg)独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。
protected boolean tryRelease(int arg)独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。
protected int tryAcquireShared(int arg)共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int arg)共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease

以非公平锁为例,这里主要阐述一下非公平锁与AQS之间方法的关联之处,具体每一处核心方法的作用会在文章后面详细进行阐述。
在这里插入图片描述

在这里插入图片描述

加锁:

  • 通过ReentrantLock的加锁方法lock()进行加锁操作。
  • 会调用到内部类Sync的lock()方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的lock()方法,本质上都会执行AQS的acquire()方法。
  • AQS的acquire()方法会执行tryAcquire方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire
  • tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。

解锁:

  • 通过ReentrantLock的解锁方法unlock()进行解锁。
  • unlock()会调用内部类Sync的release()方法,该方法继承于AQS。
  • release()中会调用tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁
  • 释放成功后,所有处理由AQS框架完成,与自定义同步器无关

通过上面的描述,大概可以总结出ReentrantLock加锁解锁时API层核心方法的映射关系

在这里插入图片描述

ReentrantLock 为例分析加锁和解锁流程

在这里插入图片描述

lock()

1、由于这是一个显示锁Lock接口的实现,所以从lock()方法进入加锁

public void lock() {
        sync.lock();
 }

可以发现调用了内部Sync对象的lock()方法

//abstract static class Sync extends AbstractQueuedSynchronizer
abstract void lock();

2、内部的Sync类也是抽象的,而lock()方法也是抽象的,所以lock应该在实现类中实现

3、分别来看两个不同的Sync实现

//static final class NonfairSync extends Sync
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
    		}
//一上来就直接先尝试修改state,成功的话则表示自己成功获取Lock,然后将排他线程属性设置为当前线程
//这其实也就是不排队的方式,一上来就去抢锁,不判断队列情况,所以很可能在抢在队列中等待的线程之前拿到锁
//如果直接cas抢不过来,则使用acquire(1),试图使用AQS中等待队列的方式,看看是排队呢还是自旋

//static final class FairSync extends Sync
        final void lock() {
            acquire(1);
        }
//公平锁则不会直接抢夺,直接使用acquire(1),直接使用AQS队列,看看需不需要构造队列,如果不需要则自旋

4、AQS的acquire()方法实现:

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

4.1、首先会调用子类实现的tryAcquire(arg),如果不成功,则尝试进入等待队列

//ReentrantLock中实现了FairSync和NonFairSync,分别实现了不同功能的tryAcquire(int arg)

//公平锁
		protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {			//首先判断当前state是否为0,为0才有直接获取锁的可能
                if (!hasQueuedPredecessors() &&			//公平锁要保证公平,就是尽可能保证先来的线程先拿锁,所以不能直接进行cas,要先看看等待队列中是否有 已经在等待的前辈们
                    compareAndSetState(0, acquires)) { //!hasQueuedPredecessors() 成立也就是hasQueuedPredecessors()返回false,代表没有前辈线程在等待,则尝试cas,如果cas也成功了
                    setExclusiveOwnerThread(current);  //则设置排他线程属性为当前线程
                  //这说明了当没有线程等待时,可以直接占有锁,也不需要管queue
                    return true;
                }
            }
      //如果c != 0,则说明已经有线程正在使用锁,那么就先看看 是不是当前线程在重入,也就是当前线程重复获得锁。这里支持重入。
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);		//将state属性进行 累加
                return true;
            }
            return false;
        }//从这里也可以看出,state只存在两种可能, 0 或者一个正数,由于是独占锁支持重入,正数可能>1,但是都必须是同一个线程给占用的state,也就是state>1的时候,state的值 必须是同一个线程累加上去的

//非公平锁,调用了父类Sync中的 nonfairTryAcquire()方法,也就是说Sync默认也是使用非公平的,所以将非公平的实现直接写在了 Sync这个抽象类中。
			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;
        }

注意,这里一定是首先判断state==0,如果state已经被别人占用了,则就无从谈起是否要判断hasQueuedPredecessors()

所以下面hasQueuedPredecessors()判断的前提,也是当前state还没有被占用

4.2 公平锁中hasQueuedPredecessors() 判断是否有等待的前辈,返回true则代表有等待的,如果返回true,那么公平锁的 tryAcquire()就要返回false

    //AQS提供了判断是否有前驱等待线程的方法
		public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
  • 先判断 h != t,这里如果返回false,也就是h=t,则直接短路返回false; h=t 代表当前根本没有初始化等待队列,也就说明在此刻并没有在队列中等待的线程(这里判断没有等待的,只是给了当前线程可以cas的机会,但是不代表cas一定成功,因为并发随时可能发生,可能有多个线程同时判断了等待队列为空,同时cas,那么只有一个cas可以成功)

  • h != t 通过了,说明h!=t,则说明等待队列已经存在了;那么就进行后半部分,也就是((s = h.next) == null || s.thread != Thread.currentThread())

  • 这是一个 或运算,前半部分 (s = h.next) == null 如果为 true,则直接短路返回,这里为true的情况可能有:

    • 此时已经有别的线程(线程b)cas成功抢到锁了,而此时还有一个线程c cas失败了,开始进入了构造queue的过程,那么就可能在初始化queue的时候,导致h.next=null(这里要结合后续的enq(Node node方法来理解))

          private Node enq(final Node node) {
              for (;;) {
                  Node t = tail;
                  if (t == null) { // Must initialize
                      if (compareAndSetHead(new Node()))
                          tail = head;
                  } else {
                      node.prev = t;
                      if (compareAndSetTail(t, node)) {
                        //在这个位置,如果是初始化完毕后,并且cas成功,那么当前head指向new Node(),tail指向传入的node,当前栈上t仍然指向head,在t.next=node之前,目前的指针缺失了head.next,此时,虽然队列已经开始初始化了,但是还差最后一步,将上一个节点(在初始化时上一个节点就是头结点,封装了一个new Node)的next指向 新加入的node
                          t.next = node;
                          return t;
                      }
                  }
              }
          }
      
    • 基于上面的代码,则(s = h.next) == null 如果为 true,很可能是其他线程已经开始enq方法,也就是开始进入构造等待队列的过程中,此时很可能有一个短暂的中间状态,虽然h != t 也就是虽然队列已经构造除了 head 和tail,并且head != tail,但是还可能差最后一步 将head.next ->node;在这个时刻可能造成 h.next == null

    • 所以这个判断解读为: 如果h!=t(已经有队列,或者至少保证已经开始构造队列了),但是如果 (s = h.next) == null 如果为 true,说明此时有其他线程已经enq成功(肯定不是自己,因为如果是自己的话在当前线程不会出现这种中间状态的临时判断,这里h.next 肯定不是Null),则就代表有前驱线程

  • h != t && ((s = h.next) == null || s.thread != Thread.currentThread()) 最后一个判断 s.thread != Thread.currentThread()) 较好理解,就是如果有队列了,但是 (s = h.next) == null 为false(代表确实已经有等待的线程了(可能是其他线程,也可能是自己这个线程),而且这个线程在enq的完毕了,已经将双向链表维护好了指向关系,所以这里看到的才 不是null),才会走到最后一个判断,这里就是判断等待的线程是不是自己;为何要进行这个判断?是因为在并发不高的场景下:比如虽然当前锁被占了,但是只有自己线程去尝试获得锁,那么可能对当前线程进行一定的自旋等待,来重复的去cas尝试;所以 判断能走到这里并且 s.thread != Thread.currentThread()) 如果是true,代表当前线程不是 老二,则肯定有前驱的其他线程在等待,如果判断走到这里,但是这里返回false,则造成这个现象的原因就是 当前线程在 acquireQueue方法中,一直自旋来尝试获得锁,所以acquireQueue会一直调用tryAcquire,从而一直判断是否有前驱,只要锁还没被释放,那么当前线程就一直会走这个s.thread != Thread.currentThread())判断,意思就是:等待队列虽然有了,但是是当前线程作为 下一个继任者一直在等,所以对当前线程来说,是没有前辈在等待的,只有一个正在执行的线程占有锁罢了。

所以总体来说,hasQueuedPredecessors()返回true(有前驱线程)的可能性有:

  • 等待队列非空(h != t成立),并且(s = h.next) == null 成立 或者 s.thread != Thread.currentThread()成
  • 也就是 如果等待队列不空,如果头节点的下一个节点是null,则说明肯定有人正在进行enq()方法的初始阶段,所以这个线程肯定比当前线程早,所以前驱线程判断成立
  • 也可能 等待队列不空,而且 头结点.next 也不是null,但是头结点的写一个节点所代表的线程也不是当前线程,那更说明有前驱线程

hasQueuedPredecessors()返回false(没有前驱线程)的可能性有:

  • h == t,则直接短路返回false,因为压根还没有等待队列,所以至少当前没有线程在等,所以线程即使并发,也应该是公平的进行cas,让总线仲裁。
  • h != t ,就是有等待队列了, 则((s = h.next) == null || s.thread != Thread.currentThread())要为false,则两个子条件都为false;也就是说(s = h.next) != null,第二个节点不是null,但是 s.thread == Thread.currentThread()成立,而当前线程就是头节点的下一个等待者,也就是当前线程就是马上要执行的线程,那么就说明没有线程排在前面

tips:双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。在acquireQueued()方法中,如果当前节点cas获取锁成功,则调用setHead(node)方法,让head->node,但是这只是让当前node来做这个虚节点

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
//setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据

acquireQueued() 和 addWaiter()

前面从 ReentrantLock的 lock()开始,分析了其内部 FairSync和 NonFairSync的lock()实现以及tryAcquire()实现

lock() --->AQS.acquire()--->子类.tryAcquire()    //在公平锁内部,特殊的分析了hasQueuedPredecessors()

下面继续分析acquire()方法中的acquireQueued()和addWaiter()

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


        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        } //构造了一个Node对象来代表当前和保存thread,nextWaiter属性用于condition的等待队列,这里没用到condition,所以传进来的是null

如果tryAcquire(arg) 返回true,即代表获取锁成功,则!tryAcquire(arg)为 false直接短路

如果tryAcquire(arg) 成立,则!tryAcquire(arg)为true,意味着 如果获取锁不成功,则进行下面的操作(入队)

static final class Node {
	       static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
}
//内部类Node中定义了几个静态变量,其中这里使用的Node.EXCLUSIVE 为null

从内向外,先分析addWaiter(Node mode)方法

/**
     * Creates and enqueues node for current thread and given mode.
     * 使用给定的模式 来为当前线程创建一个node,并加入队列,Node.EXCLUSIVE代表独占锁
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;  //先拿到tail,放到栈中的局部变量表
        if (pred != null) { 		//如果tail 非空,则代表已经有队列了
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {  //cas失败,则说明有并发线程在入队
                pred.next = node;
                return node;
            }
        }
        enq(node);   	//如果刚才拿出的tail 为null,或者 cas设置新的tail失败,则使用enq来加入队列
        return node;
    }

		//无限循环直到代表 当前thread的node加入queue,如果需要的话,则初始化queue
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;		//先拿出tail。如果tail是空,则说明还没有队列,则尝试cas 设置head为一个new Node()
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                  //cas成功之后,则暂时先让tail=head=new Node()
                  //这一步如果并发,那么就只有一个线程cas成功,失败的线程再次for循环
                    tail = head;
            } else {		//第二次for,或者一进来就发现tail !=null
                node.prev = t;		//则先把当前node的 prev指向t,也就是保存下来的tail
              //注意这里指的是 保存下来的tail而不一定是 当前真正的tail节点,因为并发随时存在,tail一直可能会变
              //然后尝试将tail设为当前node,这里cas成功,则说明从进入enq,并将t=tail保存下来之后,都没有人来修改过tail,如果失败,则需要重新获取tail,说明有的线程先来一步,已经将tail指向了自己。
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

对addWaiter(),和enq方法总结就是

  • addWaiter()首先判断一次tail是不是为空,并尝试cas设置 tail 为当前node,如果设置成功,则链表构建完毕
  • 如果addWaiter()前半部分逻辑判断失败,则进入enq
  • enq首先仍然 先把tail 保存到栈帧局部变量t中,然后判断t是不是为空来看是否需要初始化队列
  • 如果需要初始化队列,则尝试set head= new Node() 所以初始化队列时不是将head指向当前线程节点,如果成功,则将tail = head ,也就是此时tail 和head都指向一个new Node();这个Node是没有意义的一个Node作为头结点
  • 如果cas set head失败,或者set成功,则进入下一次for循环,仍然拿出tail来保存到t(每次循环都要重新拿一次tail,因为并发随时发生,t保存的tail,可能就不是真正的tail),然后尝试cas set tail= 当前线程Node ,如果成功,则当前线程入队成功,否则再次for循环,直到当前线程cas set tail 指向 当前Node未知

addWaiterenq方法中新增一个节点时为什么要先将新节点的prev置为tail再尝试CAS而不是CAS成功后来构造节点之间的双向链接?
这是因为,双向链表目前没有基于CAS原子插入的手段,如果我们将node.prev = t和t.next = node(t为方法执行时读到的tail,引用封闭在栈上)
放到compareAndSetTail(t, node)成功后执行,如下所示:

if (compareAndSetTail(t, node)) {

  node.prev = t;

  t.next = node;

  return t;

}

会导致这一瞬间的tail也就是t的prev为null,这就使得这一瞬间队列处于一种不一致的中间状态。

有如下情景:将原来的tail节点定义为nodeTail

  1. 时刻1:tail=t 都指向了nodeTail,线程A进入,进行了如上的CAS,将tail指向了node,然后马上cpu切换到线程B
  2. 时刻2,线程B进入cpu,获取t=tail ,这时线程b的tail指向的是node,但是由于线程a里没有设置node的prev,所以此时tail的prev=null,也就是短暂的队列不一致。
  3. 所以将node.prev=t放在cas之前,但是这样会不会导致node的prev会出错呢?
  4. 其实这样最多只会造成当前这个线程的node可能在一小段时间内。prev指向的不是真正的tail。(多并发下,tail常常变。)
  5. 其实不会,如果CAS成功,其实prev确实指向了上一个tail,那么tail.next也就指向node
  6. 如果cas失败,则自旋,会一直循环更新node.prev,直到CAS成功指向某一时刻的tail,tail.next也指向node,从而保持双向一致。
  7. 其实在node.prev=t放在cas之前,则保证了多并发场景下,维持链表上的数据最低的一致(也就是不会出现上述在一瞬间prev=null,使得链表断掉这种情况)。保证后续从tail开始从后向前prev遍历的时候,链表可用,不出现prev=null的状态。其实也就是,只要tail cas 设置成功了,则说明enq的一次for循环中,tail还没有并发被修改,所以当前线程设置的prev一定是有效的,也就是originTail节点,所以这个链表从后向前必定是有效的;保证了从tail开始遍历的话,肯定是可以遍历完整个链表,而且由于tail设置之前prev就有了,所以prev一定能保证链表完整
  8. 这也解释了为何unparkSuccessor 使用prev来遍历链表。
    //这里传入的node,就是addWaiter()返回的Node对象,也就是当前线程的Node
//arg 一般为1,代表当前获取资源的数目

		final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {				//for循环来保证自旋,在线程醒来后,仍然for循环尝试获取锁
                final Node p = node.predecessor();		//获取前驱
                if (p == head && tryAcquire(arg)) {		//如果当前节点就是head的下一个,则说明有资格获取锁,则允许tryAcquire();
                  //这种场景可能是并发较少,比如当前线程刚进来,只有一个线程正在执行,就是头结点所在的线程
                  //也可能是头结点线程执行完了,把当前线程唤醒,然后当前线程来tryAcquire一次就成功了
                  
                  //成功之后来setHead
                    setHead(node);
                    p.next = null; // help GC,将原来的head从链表中拆除
                    failed = false;
                    return interrupted;   //返回中断状态,因为当前方法不支持中断异常
                }
              //如果上一个if失败,则判断是否应该park,并且检查中断状态
              //如果这里返回false,则重新进入for循环,判断p == head && tryAcquire(arg),不成功的话再判断shouldParkAfterFailedAcquire,第二次shouldParkAfterFailedAcquire 通常就会返回true了,因为前一个Node已经被我们设为了Signal
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                  //这里如果parkAndCheckInterrupt()返回中断状态,则将这个状态返回出去
                    interrupted = true;
            }
          //出任何异常的话,则退出竞争
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


//pred是前驱节点,node是当前节点
//这里pred有可能就是head,当前节点由于虽然是head.next,但是其cas失败了,所以也应该进入此方法进行判断,通常这时候head节点很可能不是signal,所以当前node还有一次机会也就是这里casSetHead Ws=Signl后返回false,重新cas尝试一次
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
  //如果前驱节点状态就是SIGNAL,说明前驱都处于 等待通知的状况,那么当前node就老实排队
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {			//前驱节点处于cancel
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do { 		//将node前驱指向 pred的前驱,意思就是将pred从队列中拿掉
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);  //一直循环,拿掉那些cancel状态的Node,直到碰到一个有效的Node停止。
          //拿掉一部分cancel Node后。注意可能只是一部分,因为cancel Node可能不连续
          //找到一个有效的prev后,构建双向链表
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
          //如果前驱节点既不是Signal,也不是cancel,那么就只能是0或者propagate,可能前一个节点就是0,那么就把前一个节点设为Signal,让他处于等待signal的状态,自己则返回false,重新在试一次看看能不能满足 p=head并且tryAcquire()成功,如此往复
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }


		//执行park,如果park醒来,则返回中断状态,并且重新回到上一层方法进行for循环,尝试获取锁
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }


//最终,如果 if判断都成立了,也就说明acquireQueued 最终也从park阻塞中返回了中断状态
//虽然acquire会成功执行完毕,但是中断状态需要保留
//再次执行selfInterrupt 来将当前线程中断一次,来让当前线程对象 内部的中断状态属性设置上
//可以让外部调用者 使用这个属性进行一定的判断逻辑
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

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

很显然,如果只有一个线程在等,那么这个线程就是作为一个 head = new Node()的下一个节点,而这个线程所在的Node,由于没有后来者给他设置ws,所以其一直ws 一直为0

但是这也没关系,因为这个线程一直就是head.next;所以其会一直自旋,根本不用通过 SIGNAL这种标识来唤醒;

acquireQueued方法流程图为:

在这里插入图片描述

shouldParkAfterFailedAcquire流程:

在这里插入图片描述

也就是说,只要进入了shouldParkAfterFailedAcquire,这个方法由于不响应中断,所以最多将中断保留,但是不抛出异常,所以中断了只是让线程醒来,然后马上去重新尝试获取锁,在没有别的异常发生情况下,acquire()方法一定能在 有限次 park()和唤醒之后,成功获得锁(因为不会中途被中断打断)。

p == head && tryAcquire(arg),有没有可能tryAcquire(arg)失败?

有可能,在非公平锁的情况下,有可能tryAcquire并发失败。

到此为止,ReentrantLock的lock()方法结束,一般的lock(),使用的是不响应打断的方式获取AQS state属性,中间伴随着对 AQS中等待队列的操作、遍历和前驱结点,头尾节点等判断;最终在高并发场景下保证了能够顺利获取临界资源和顺利进入等待。

unlock()

    public void unlock() {
        sync.release(1);		//仍然调用同步器对象的方法,只不过release方法在AQS抽象类中就有实现了
    }

//release(),对应acquire()

//AQS
    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     * 以独占锁形式释放资源,如果tryRelease返回true,则其实现就是通过解锁一个或多个线程
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {			//如果release了所有的资源,则进入if body
            Node h = head;					
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


//Sync实现了tryRelease()
				protected final boolean tryRelease(int releases) {
            int c = getState() - releases;			//查看当前的state和release数目之间的差值
            if (Thread.currentThread() != getExclusiveOwnerThread())		//如果当前线程不是 占有锁的排他的线程,则报异常
                throw new IllegalMonitorStateException();
          //如果走到这,说明当前线程就是 独占锁线程,所以可以执行以下具体的state修改操作
            boolean free = false;
            if (c == 0) {							//如果这次release完了,剩余的资源数为0,则表示全部释放
                free = true;				//表示释放完毕
                setExclusiveOwnerThread(null);			//exclusiveOwnerThread对象改为null
            }
            setState(c);												//不管是否释放完,都要修改状态,因为可以重入,所以应该就可以release多次,支持一次release一部分,或者一次 -1这种操作。
            return free;
        }

release(int arg) 方法中 if (h != null && h.waitStatus != 0) 对这个判断的解读:

  • h != null,如果这个不满足,即h==null,则直接短路,不进行后续节点的唤醒;因为等待队列都没有,就没必要唤醒
  • h != null满足了,再看 h.waitStatus != 0,首先要知道 什么时候 h.waitStatus == 0, 等于0只可能出现在队列最开始的情况,或者是竞争极为不激烈,没有出现队列中排队数 >2的情形(大于2的话,队列中第二个等待的线程会在park()之前 将前驱设为Signal,所以前驱如果获得锁并 成为head,那么他的ws一旦为Signal,那么后继一定是有人的
  • 所以如果h.waitStatus == 0,所以一定是竞争不激烈,只有一个线程等待锁,那么这个等待线程也就不需要唤醒,因为其会尝试shouldParkAfterFailedAcquire两次(第一次将pred改为signal,第二次才会park),所以这里ws=0说明 后继线程还在run呢,还没有到park的时候,所以不必要唤醒;只有当前h.waitStatus != 0 成立,说明当前head代表的node肯定是被后继给标记为SIGNAL了,而且后继很有可能在标记 前驱为SIGNAL之后就park()了;所以此时应该进入unparkSucessor()的逻辑;
//传入的node,一般为上一步 保留在栈帧中的 h 变量,也就是当前的head
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
  		//如果当前是signal,则将当前头节点 ws设为0
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
  //先找h.next,如果是null,或者 处于Cancel状态
  //s==null 很可能出现在enq()方法、或者在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
          //一路从tail向前找到最前面的 t.waitStatus <= 0 的节点,当做当前head的next
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
  //对找到的那个线程,使用unpark,也就是颁发一个许可。
        if (s != null)
            LockSupport.unpark(s.thread);
    }

这里遗留了一个问题,什么时候compareAndSetWaitStatus(node, ws, 0);会失败呢?

cancelAcquire(Node node)

shouldParkAfterFailedAcquire()方法中,有对Cancel状态节点的判断,那么什么时候会生成CANCELLED状态节点?

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
          //如果没有正常在上面的for循环中将failed改为false,那么就是出异常了
            if (failed)
                cancelAcquire(node);
        }
    }
// java.util.concurrent.locks.AbstractQueuedSynchronizer

private void cancelAcquire(Node node) {
  // 将无效节点过滤
	if (node == null)
		return;
  // 设置该节点不关联任何线程,也就是虚节点
	node.thread = null;
	Node pred = node.prev;
  // 通过前驱节点,跳过取消状态的node,直到找到一个正常的前驱
	while (pred.waitStatus > 0)
		node.prev = pred = pred.prev;
  // 获取过滤后的前驱节点的后继节点
	Node predNext = pred.next;
  // 把当前node的状态设置为CANCELLED
	node.waitStatus = Node.CANCELLED;
  // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
  // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
	if (node == tail && compareAndSetTail(node, pred)) {
		compareAndSetNext(pred, predNext, null);
    
    //如果当前不是尾节点,或者更新尾结点cas失败(可能是有并发入队的线程cas抢占了)
    //则说明目前不需要这里来进行 casSetTail操作,也不需要设置 prev.next为null
	} else {
		int ws;
    // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
    // 如果1和2中有一个为true,再判断前驱节点的线程是否为null
    // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
		if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
			Node next = node.next;
			if (next != null && next.waitStatus <= 0)
				compareAndSetNext(pred, predNext, next);
		} else {
      // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
			unparkSuccessor(node);
		}
		node.next = node; // help GC
	}
}

对条件 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null)的解读:

  • pred != head 代表前驱节点不是head,如果是head,则进入else,直接唤醒后继即可。因为此时node.next就是事实上的队列的第一个等待者了

  • pred != head满足,然后如下要满足:那么意思就是说,前驱节点要么已经是Signal了,要么cas 他为signal成功;已经是signal好理解,应该是pred的后继线程设置的(pred和node之间也很可能夹杂了好几个cancel状态的node),也有可能不是Signal,因为pred原来的那些后继都cancel了,还没来得及给pred设置signal;如果cas失败,则可能是目前有另一个线程也在走cancelAcquire()方法,也找到这个pred节点了,然后把他设为了signal(因为当前节点已经是cancel了,所以如果另一个线程也cancel,则很可能也找到同一个pred节点)

  • 所以如果这个 || 条件不满足,则代表现在可能有并发cancel的,干脆直接unparkSuccessor,从tail开始往前找一个线程先唤醒了,让唤醒的这个线程来为前面的pred节点设置ws状态,哪怕再检查一次发现已经是signal了,再次park就是了

  • 而且 || 条件不满足,直接唤醒后继线程unparkSuccessor的话,也会从tail开始遍历,将中间cancel状态的node拿掉一部分

  • pred.thread != null 表示pred是一个有意义的节点,否则=null的话(很可能是这个pred所在的线程也开始cancel了,或者pred就是head?但是进入到这了,说明pred!=head已经满足了,否则短路),则给其设置next则没意义,不如直接唤醒一个线程,让线程在unparkSuccessor流程和 acquireQueued流程中摘除这些cancel的节点

    ((ws = pred.waitStatus) == Node.SIGNAL ||
     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
    

如果这一长串条件满足,则说明 至少在当下pred是一个正常,并且可以被signal的节点,所以给这个pred设置next=node.next(当然,这里也可能失败,毕竟还是可能并发的 排在后面的node进行cancel)

最后,要执行node.next = node;来断开next连接

其实一直看到的是,node的prev似乎一直没断开,这样应该是为了让链表一直处于可遍历的状态?这样其实也是可以gc的,即便node.prev引用了别的node,但是没有其他的node再指向这个节点,所以应该是可以gc

对cancelAcquire()方法的总结:

  • 获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。
  • 根据当前节点的位置,考虑以下三种情况:

(1) 当前节点是尾节点。

(2) 当前节点是Head的后继节点。

(3) 当前节点不是Head的后继节点,也不是尾节点。

根据上述第二条,我们来分析每一种情况的流程。

1、当前节点是尾节点

在这里插入图片描述

2、当前节点是Head的后继节点

在这里插入图片描述

3、当前节点不是Head的后继节点,也不是尾节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tTBQKxRQ-1587525942475)(/Users/lvchentao/Desktop/cloudPoint/高并发 多线程/AQS源码分析/45d0d9e4a6897eddadc4397cf53d6cd522452.png)]

通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作?

仍然是考虑极端情况的并发:

1、在当前node执行cancel时,其pred很可能也在执行cancel;所以当前node执行compareAndSetNext(pred, predNext, next);之后,很可能pred已经变为cancelled状态;

2、当前node.next节点很可能此时刚入队,并且进入了shouldParkAfterFailedAcquire,然后开始向前遍历,找到一个有意义的节点,很显然,会跳过当前node,也会跳过pred

3、如果此时当前node线程,在cancelAcquire()方法中,再去同步的操作 node.next 的prev指针,很可能将prev指针 改成指向pred;这造成了指向一个 被移除队列的Node

4、所以AQS中对链表的遍历都是通过prev,所以不会出现在多个地方并发处理prev造成不安全

5、对prev的处理,一般都在shouldParkAfterFailedAcquireshouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化(之前的节点都处于park状态),因此这个时候变更Prev指针比较安全。(此时并发较少)

do {
	node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);

参考文章:

从ReentrantLock的实现看AQS的原理及应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值