ReentrantLock&FairSync源码分析-AQS排他锁并发控制类实现分析

AQS是concurrent包中同步类的核心,AQS的acquire&release和acquireShared&releaseShared用模板模式定义了并发逻辑的实现,并发控制类通过继承AQS,实现tryAcquire&tryRelease和tryAcquireShared&tryReleaseShared,就能构建出新的并发控制逻辑(ReentrantLock,CountDownLatch等),或使用基于AQS实现的并发控制类构建出功能更复杂的并发控制类(CyclicBarrier,ArrayBlockingQueue等).

AQS有两种锁模式:排它锁和共享锁。排他锁性质的并发控制类需要实现AQS的tryAcquire和tryRelease方法,共享锁性质的并发控制类需要实现tryAcquireShared和tryReleaseShared方法。下面基于ReentrantLock&FairSync的源码分析,说明如何利用AQS排它锁实现并发控制类。

分析过程先说明如何基于AQS实现ReentrantLock,最后解析整个过程中的细节。

ReentrantLock.lock(): void {
  sync.lock(); //sync是FaiySync实例,最终调用AQS.acquire(1)
}
ReentrantLock.unlock(): void {
  sync.release(1); //sync是FaiySync实例,最终调用AQS.release(1)
}

lock和unlock最终调用AQS的acquire和release方法,其代码如下:

AbstractQueuedSynchronizer.acquire(int arg): void {
  //1、tryAcquire实现加锁,返回是否成功,由子类实现;
  //2、tryAcquire失败,执行addWaiter:new Node(Thread.currentThread, EXCLUSIVE),加入AQS的waiter
  // 链(AQS waiter链特点:head对应的是当前正在执行的线程,但不持有有效数据,head.next是unlock默认唤醒的线程的节点,具体实现后文分析);
  //3、执行acquireQueued:当前线程暂停进入waiting状态,被唤醒并获得cpu时间分片后重新尝试获取锁;
  if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    //置当前线程的interrupt状态
    selfInterrupt();
}

AbstractQueuedSynchronizer.release(int arg): boolean {
  //tryRelease逻辑由子类实现,返回释放锁是否成功:false不代表失败,在重复加锁的情况下,false表示还未释放所有的锁;
  if (tryRelease(arg)) {
    //head为AQS等待链的头
    Node h = head;
    //waitStatus==0:0默认值,head的waitStatus为0表示AQS的waiter链只有head节点(如果有后续节点,一定会将head的waitStatus置为其他状态,具体逻辑稍后分析)
    if (h != null && h.waitStatus != 0)
      //唤醒AQS waiter链中的某个线程;
      unparkSuccessor(h);
    return true;
  }
  return false;
}

从上面的代码可以发现AQS acquire和release是模板模式的实现,具体的子类实现只需要编写加锁(tryAcquire)和解锁(tryRelease)逻辑,而不用操心加锁失败后线程如何暂停最终又如何被唤醒,和解锁成功后如何唤醒等待线程的实现逻辑,极大简化了并发控制类的实现难度。下面是FairSync具体的tryAcquire和tryRelease的代码实现分析:
FairSync.tryAcquire(int acquires): boolean {
  final Thread current = Thread.currentThread();
  int c = getState();
  //state出事化值为0,被别的线程加锁后会大于0,故c == 0,表明没有线程加锁
  if (c == 0) {
    //1、!hasQueuedPredecessors:公平判断。如果AQSwaiter链为空,或waiter链第一个待唤醒Node的thread是当前线程,则结果为true。此判断实现公平锁的逻辑。去掉即

           //     是NonFairSync的tryAcquire实现逻辑;
    //2、cas设置state(此操作执行的时候是没有加锁的,故需要cas执行),如果成功,表明当前线程加锁成功;如果失败,表明在getState操作到cas操作
    //    期间内,有别的线程加锁成功;
    if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
      //设置持有排它锁的线程,判断中的cas操作成功,加锁已经成功,故set无需cas操作
      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;
}

FairSync.Sync.tryRelease(int releases): boolean {

      //解锁的过程中,线程是持有锁的,故无需cas操作
  //加锁次数递减
  int c = getState() - releases;
  if (Thread.currentThread() != getExclusiveOwnerThread())
    throw new IllegalMonitorStateException();
  boolean free = false;
  //加锁次数为0,表明当前线程已经完全释放了锁
  if (c == 0) {

           //返回解锁完成,AQS根据此返回值唤醒锁上的等待线程
    free = true;
    setExclusiveOwnerThread(null);
  }
  //更新state
  setState(c);
  return free;
}
从分析中可以看到,加锁解锁的实现是通过操作AQS的state变量来实现的(基于AQS实现的并发控制类实现逻辑都是通过操作state来实现相关并发语义的)。

到此,我们基本分析完了ReentrantLock的实现,可以看到,如果要基于AQS排它锁实现一个并发控制类,需要:

1、根据自己的控制逻辑实现tryAcquire和tryRelease;

2、tryAcquire和tryRelease围绕state变量实现;

3、在tryAcquire实现内,加锁成功前的更新操作要调用AQS提供的cas完成;

4、tryAcquire&tryRelease内不能有异常发生,否则会导致一些不可预测的情况发生;可试想ReentrantLock.unlock的tryRelease发生了异常会导致什么情况;

以上分析仅仅告诉我们怎么基于AQS实现自己的并发控制类,未涉及整个加锁流程中加锁失败后的逻辑,与解锁成功后如何唤醒线程的逻辑。如果有兴趣,可以继续看下面的分析。

前面的分析提到AQS.accquire中,加锁失败后会执行addWaiter和acquireQueued操作,addWaiter会将当前线程加入到一个待唤醒线程的waiter链,acquireQueued会暂停线程的执行并等待被唤醒后重新执行tryAcquire,下面是代码分析:

//将当前线程加入waiter链
AbstractQueuedSynchronizer.addWaiter(Node mode): Node {
  //mode = Node.EXCLUSIVE,排他模式
  Node node = new Node(Thread.currentThread(), mode);
  Node pred = tail;
  //如果当前waiter链已经有元素
  if (pred != null) {
    node.prev = pred;
    //cas设置当前线程的节点为tail
    if (compareAndSetTail(pred, node)) {
      pred.next = node;
      return node;
    }
  }
  //在循环中cas设置tail
  enq(node);
  return node;
}

//在循环中使用cas操作将当前thread加到waiter链的队尾
AbstractQueuedSynchronizer.enq(Node node): void {
  for (;;) {
    Node t = tail;
    //如果waiter链为空,则初始化 head = tail = new Node()
    if (t == null) { // Must initialize
      if (compareAndSetHead(new Node()))
        tail = head;
      } else {
        //否则cas设置tail = node
        node.prev = t;
        if (compareAndSetTail(t, node)) {
          t.next = node;
          return t;
        }
      }
    }
  }

}
执行完addWaiter后,一定会将当前线程加入到waiter链,比如thread1和thread2调用同一个ReentrantLock实例的lock&unlock,如果thread1调用ReentrantLock.lock成功,那么在thread1未unlock前,thread2调用lock并执行完addWaiter后构建了如下的双向waiter链:head = Node(null,null, SIGNAL)->Node(thread2, EXCLUSIVE,null) = tail。

接着分析如何暂停线程,和线程如何在被唤醒后重新获取锁,具体源码如下:
AbstractQueuedSynchronizer.acquireQueued(final Node node, int arg): boolean {
  boolean failed = true;
  try {
    boolean interrupted = false;
    for (;;) {
      //node.prev,null时抛出空指针异常。为什么:null时表示node是头结点,头结点可以看作
      //当前运行的线程对应的Node,不需要后续操作
      final Node p = node.predecessor();
      //如果当前线程对应的Node是head后的第一个Node,并且加锁成功,当前线程执行进入临界区
      if (p == head && tryAcquire(arg)) {
        //将node设置为头结点(此处代码可以看到,head节点即是当前线程对应的Node,只不过head节点的thread这些信息会清除)
        setHead(node);
        p.next = null; // help GC
        failed = false;

                       //返回线程中断标志
        return interrupted;
      }
      //需要暂停线程;暂停线程并保存中断信号
      if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
        interrupted = true;
      }
    } finally {
      //for循环内抛出了异常
      if (failed)
        //更新waiter链元素,如果有必要,唤醒waiter链中的某个Node的线程
        cancelAcquire(node);
    }
  }

}
AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire(Node pred, Node node): boolean {
  int ws = pred.waitStatus;
  //如果node前面节点的waitStatus为SIGNAL,那当前节点对应的线程肯定需要暂停:因为AQS是按waiter
  //链的先后顺序唤醒线程的。反过来说,如果一个线程暂停了,那它对应Node.prev.waitStatus是SIGNAL(除非另一个线程修改了)
  if (ws == Node.SIGNAL)
    return true;
  //如果node前面节点的waitStatus为CANCEL(>0的值只有CANCEL)
  if (ws > 0) {
    //以node为起始,向前删除waiter链中CANCELLED状态的节点
    do {
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
  } else {
    //将node前面节点的waitStatus置为Node.SIGNAL:故acquireQueued的下一次循环调用此方法,会返回true,最终暂停线程
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
  return false;
}
AbstractQueuedSynchronizer.parkAndCheckInterrupt(): boolean {
  //暂停线程。linux机java线程是映射到内核线程的,线程的调度最终由内核完成
  LockSupport.park(this);
  return Thread.interrupted();
}

从上面的分析中,大致归纳出一下tryAcquire失败后的代码逻辑:
1、将当前线程加入AQS waiter链;
2、进入循环:正常/异常
  2.1、当前线程对应的Node是head.next,并且加锁成功,则更新waiter链的head为当前线程对应的node,返回继续执行后续的业务代码。此处tryAcquire两种情况可以执行成功:在当   前线程第一次tryAcquire失败到本次tryAcquire期间,持有锁的线程释放了锁;持有锁的线程释放了锁,被唤醒线程成功加锁。
  2.2、2.1的判断失败,判断当前线程是否需要暂停(node.prev.waitStatus == SIGNAL)(实现逻辑在for循环内,正常情况下for循环的第二次一定会返回true)。如果需要则暂停线程   的执行;不需要,重复2.1;
  2.3、waiter链中的某个Node被持有锁的线程唤醒,重复2.1;

思考一个问题:在acquire方法内,tryAcquire失败后,为什么不立即暂停线程,反而在最终暂停前,当前线程至少还有一次机会再次tryAcquire?

前面看到,acquireQueued的for循环外部有finally,finally用于保证for循环内部发生异常的情况下,unpark信号量不会丢失。代码分析如下:
AbstractQueuedSynchronizer.cancelAcquire(Node node): void {
  if (node == null)
    return;
  node.thread = null;
  Node pred = node.prev;
  //以node为起始,往前删除waiter链中CANCELLED状态的节点
  while (pred.waitStatus > 0)
    node.prev = pred = pred.prev;
  Node predNext = pred.next;
  //发生异常的线程对应的节点waitStatus置为CANCELLED
  node.waitStatus = Node.CANCELLED;
  //如果当前节点是尾节点,更新tail和head.next
  if (node == tail && compareAndSetTail(node, pred)) {
    compareAndSetNext(pred, predNext, null);
  } else {
    int ws;

    //如果异常节点不是head.next,并且异常节点前面有等待唤醒的节点(waitStatus判断),并且异常节点前面节点是有效的(thread判断);
    if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))

                 && pred.thread != null) {

      //删除node
      Node next = node.next;
      if (next != null && next.waitStatus <= 0)
        compareAndSetNext(pred, predNext, next);
    } else {      

      //否则以异常节点为开始,唤醒等待链中的某个节点
      unparkSuccessor(node);

    }
    node.next = node; // help GC
  }
}
acquireQueued执行后,得到如下结果:
1、正常情况下

  1.1 当前线程在本次cpu时间分片内获取到了锁tryAcquire成功;

  1.2 被暂停(并等待被唤醒重新获取锁);

2、异常情况下,发生异常的Node从waiter链中删除;如果有必要,以node为起点查找并唤醒某个线程;

再看释放锁的代码: AQS.release调用参数值为head

AbstractQueuedSynchronizer.unparkSuccessor(Node node): void {
  int ws = node.waitStatus;
  if (ws < 0)

           //注意此操作,对cancelAcquire影响
    compareAndSetWaitStatus(node, ws, 0);
  Node s = node.next;
  //node是tail(tail.next == null)或第一个待唤醒的node.waitStatus为CANCELED
  if (s == null || s.waitStatus > 0) {
    s = null;
    //从tail往前查找,知道找到waiter链最前面waitStatus<=0的非head Node
    for (Node t = tail; t != null && t != node; t = t.prev)
      if (t.waitStatus <= 0)
        s = t;
  }

  if (s != null)
    //最终由内核唤醒线程
    LockSupport.unpark(s.thread);
}

释放锁的逻辑相对简单,从waiter链的head开始查找到第一个waitStatus <= 0的Node(从tail查找的结果是一样的);如果有,唤醒这个Node的thread;并且head.waitStatus=0;

前面提到acquire操作中tryAcquire失败后线程暂停前至少会再执行一次tryAcquire,个人是这么认为的:基于jvm统计数据发现实际应用中锁冲突的情况是比较少的(如果你的应用有很多,那你应该考虑优化),即使发生了锁冲突,大部分情况下锁是能被快速释放的,因此编码者认为在有限时间内自旋(浪费cpu)付出的代价是小于直接暂停线程导致线程上下文切换的代价的。自旋锁的方式认为通过有限消耗cpu带来的性能提升会大于线程调度带来的性能替身,自旋在jvm里有许多的应用,如偏向锁膨胀为轻量级锁直至重量级锁的过程中,也采用了自旋。

最后,给出一张ReentrantLock.lock大致的流程图:

posted @ 2016-05-25 18:07  ze2200  阅读(651)  评论(0)    收藏  举报