AQS之独占锁源码阅读

62 阅读11分钟

AQS的逻辑总是看了就忘,于是把自己的理解写成文字和图示,记录下来。先从简单的独占锁lock, lock()入手,从易到难来学习AQS。花了不少时间,希望有所帮助。

java.util.concurrent包下的大多数同步器(lock, barriers, 等)都是基于同列同步器AbstractQueuedSynchronizer(简称AQS)类来实现的,包括Java中的ReentrantLock, ReentrantReadWriteLock, CountDownLatch等。而AQS本身则是基于Java中volatile变量的读/写和CAS来实现的。

​ 下面借用j.u.c包下的可重入锁ReentrantLock的实现,从源码的角度来看看AQS中锁的获取acquire()和释放release()以及线程的await() 和signal是怎样的一个流程。

###ReentrantLock的使用和源码

​ 首先要说明的是,ReentrantLock是一个互斥锁

​ 开始看源码之前,我们先看一下Java文档中给出的ReentrantLock的使用方式:

class X {
  private final ReentrantLock lock = new ReentrantLock();
  // ...
  public void m() {
    lock.lock(); // block until condition holds
    try {
      // ... method body
    } finally {
      lock.unlock();
    }
  }
}

最简单的使用分为三步,首先我们new一个ReentrantLock对象,然后在对应的方法里面使用这个对象,调用lock(), unlock().下面我们结合ReentrantLock的公平锁和AQS的源码来看一下代码是如何执行的:

​ ReentrantLock的代码稍微省略,只保留和本次相关的代码(也就是公平锁相关的。公平锁和非公平锁也只有细微的差别),结构大致如下:

// ReentrantLock是一个互斥锁 (mutual execlusive lock)
public class ReentrantLock implements Lock, java.io.Serializable {
  // 用final修饰的一个内部静态类Sync的实例
	private final Sync sync;
  // Sync是AQS的子类,
  // 注意Sync是一个抽象类,因为具体的实现方法是它的子类:NonfairSync类和FairSync类
  abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();
    
    // 非公平的获取锁方法,供非公平锁NonfairSync类使用的,此处略
    final boolean nonfairTryAcquire(int acquires) {
      // code ...
    }
    
    // 重写父类AQS的tryRelease()抽象方法
    protected final boolean tryRelease(int releases) {
      // 每次释放锁的时候,AQS.state 减去当前次数release,直至为0
      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
      boolean free = false;
      // 当前线程获取了锁n次,释放了锁n次,至此,当前线程已经不再持有这个锁。
      if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
      }
      setState(c);
      return free;
    }
    // ... 其他方法:1.用来检测同步状态的 2. 序列化相关
  }
  
  // 非公平的获取锁,Sync的子类。此处略
  static final class NonfairSync extends Sync {
    // 代码略
  }
  
  static final class FairSync extends Sync {
    // 这个方法提供给ReentrantLock,供外部调用
    final void lock() {
      // 调用AQS的acquire方法
      acquire(1);
    }
    
    // FairSync是Sync的子类,因此也是AQS的子类,这里重写AQS的抽象方法tryAcquire()
    // 这个方法是我们的重头戏,因此我连英文注释也带上了 ^_^
    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      // 获取当前同步器AQS实例的状态
      int c = getState();
      // 同步器AQS的state为0,表明当前锁没有被任何线程持有
      if (c == 0) {
        // !hasQueuedPredecessors(): 当前sync queue非空(有其他线程在等待获取锁),而且当前线程没有持有锁
        // !hasQueuedPredecessors() 为true的话,说明当前线程在sync queue的队首,或者sync queue是空的。既然没有其他线程在等待获取锁,也没有其他线程持有锁,那么当前线程可以获取锁了,compareAndSetState(0, acquires)方法尝试获取锁。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
          // 设置锁的所有者线程是当前线程。
          setExclusiveOwnerThread(current);
          return true;
        }
      }
      // c > 0,已经有线程持有锁了:
      // 如果当前线程是持有锁的那个线程,那么通过增加AQS的state的值来增加获取当前线程锁的次数。
      else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
          throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
      }
      // 已经有其他线程持有锁了。
      return false;
    }
  }
  
  // 加锁,供外部使用
  public void lock() {
    // 调用内部Sync的lock方法,如果是公平锁,则调用FairLock.lock()
    sync.lock();
  }
  
  // 解锁,供外部使用
  public void unlock() {
    // 调用AQS的release方法
    sync.release(1);
  }
  // 其他代码:1. timed lock, interruptable lock
  // 					2. condition variable方法
  // 					3. 其他提供同步检查的方法 等
}

​ 从ReentrantLock的使用和代码逻辑可以看出,核心调用的AQS的方法有2个,即 acquire() 和release():acquire用来获取锁,release用来释放锁。

ReentrantLock获取锁的流程大致如下图所示

ReentrantLock公平锁流程图

​ 一个线程在获取ReentrantLock中的锁时,如果有在此之前等待的线程, 那么当前线程无法获取锁,会直接加入到同步器的同步队列中,直到轮到它获取锁。这里有一个比较特殊的情况就是,ReentrantLock锁没有被持有,但是当前线程(还未进入同步队列)也不能尝试获取锁,是因为在同步队列中有其他的线程在排队等候获取锁,这就是公平锁体现出“公平”的地方。同步队列中的前一个节点释放锁和后一个节点获取锁这中间不是无缝衔接的,会有一定的时间差(这也是非公平锁实现“非公平”的地方)。

###AQS中的acquire()和release()

​ 在看acquire()方法和release()之前,我们需要AQS中结构,也就是我们的线程的如何在AQS中最终获得互斥锁的?下面是AQS的结构图(我们目前讲解的部分暂时不设计到Condition部分)。

​ 争抢锁成功的线程,会直接通过同步器,进入到临界区代码块。争抢锁失败的线程则会被加入到Sync Queue同步队列的队尾,排队等待争抢锁。每个新节点在加入同步队列的时候,都会将自己前一个节点的状态设置为SIGNAL,表示自己需要被唤醒(shouldParkAfterFailedAcquire()方法中的逻辑),然后陷入休眠。每个节点在获取锁了之后,会释放锁的时候,都会唤醒自己的下一个节点(当前节点的next指向的节点)来争抢锁。这样就形成了一个动态的双向FIFO队列。

AQS结构图
下面我们将看看AQS中的acquire()和release()方法。

​ 先看acquire()获取锁的方法,相关代码如下:

class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
  
  static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
    //用来获取当前节点的前一个节点
    final Node predecessor() throws NullPointerException {
      Node p = prev;
      if (p == null)
        throw new NullPointerException();
      else
        return p;
    }
    // 构造方法,常量,是否共享锁 等
    // ....
  }
  
  public class ConditionObject implements Condition, java.io.Serializable {
  }
  
  private transient volatile Node head;
  
  private transient volatile Node tail;

  private volatile int state;

  public final void acquire(int arg) {
    // 这里调用子类实现的tryAcquire()方法,如果获取锁失败就会调用acquireQueued()方法
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      selfInterrupt();
  }
  
  // 添加Node到同步队列。
  private Node enq(final Node node) {
    for (;;) {
      Node t = tail;
      // 如果tail节点为空,那么当前同步队列为空,需要先初始化队列
      if (t == null) { // Must initialize
        // 队列的初始化, head= tail = new Node()。
        // 如果第一次进来,发现队列没有初始化,会先走这部分代码。然后继续for循环,走下面的else设置尾节点
        if (compareAndSetHead(new Node()))
          tail = head;
      } else {
        node.prev = t;
        if (compareAndSetTail(t, node)) {
          t.next = node;
          // 这里是唯一出口,如果走不到这里,就无限循环
          return t;
        }
      }
    }
  }

  // 添加新节点到同步队列sync queue.
  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) {
      node.prev = pred;
      if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
      }
    }
    // 如果上面的添加失败,再走enq()的逻辑,无限循环添加,直到成功。
    enq(node);
    return node;
  }
  
  // 设置头结点的方法,将当前头结点的prev, thread都赋值为null
   private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

  // addWaiter()之后,当前线程的新节点已经被加入到同步队列的队尾。
  // 然后走这部分流程
  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)) {
          // 同步队列中的当前线程获取了锁,那么更新当前队列的head结点为当前节点。
          // 直接获取锁,没有经过同步队列的线程,是不会对这个同步队列造成影响的,直接进入了临界区。
          setHead(node);
          // 前一个head节点的next指向为null,至此,p的prev,thread, next,都指向null.
          // 由于不在等待队列中,因此nextWaiter也为null
          p.next = null; // help GC
          failed = false;
          return interrupted;
        }
        // shouldParkAfterFailedAcquire()设置当前节点的prev节点的状态,在自己睡眠之前,设置前一个节点的waitStatus = CONDITION,当前一个节点是头节点要释放锁时,唤醒自己。
        // parkAndCheckInterrupt() 调用LockSupport类的park()方法,休眠当前线程,禁止当前线程被调度。
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
          interrupted = true;
      }
    } finally {
      // 如果最终失败了,那么就取消当前节点。包含以下几步:
      // 1. 当前节点的waitStatus = CANCELLED.
      // 2. 如果当前节点是最后一个节点,那么设置倒数第二个节点为尾节点
      // 3. prev节点不是head且是一个有效的节点(这里有多个条件),那么将prev节点和自己的next节点连接起来。否则,就unparkSuccessor()唤醒自己的next节点
      if (failed)
        cancelAcquire(node);
    }
  }
  
  // 取消获取锁的动作
  private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
      return;

    node.thread = null;

    // 无限循环,直到最前面的一个没有被取消的prev节点为止。
    // Skip cancelled predecessors
    Node pred = node.prev;
    while (pred.waitStatus > 0)
      node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // waitStatus是一个volatile修饰的变量
    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
      compareAndSetNext(pred, predNext, null);
    } else {
      // If successor needs signal, try to set pred's next-link
      // so it will get one. Otherwise wake it up to propagate.
      int ws;
      if (pred != head &&
          ((ws = pred.waitStatus) == Node.SIGNAL ||
           (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
          pred.thread != null) {
        // 如果最终找到的有效前节点满足上述条件,那么将队列中pred和next的相互连接起来。
        Node next = node.next;
        if (next != null && next.waitStatus <= 0)
          compareAndSetNext(pred, predNext, next);
      } else {
        unparkSuccessor(node);
      }

      node.next = node; // help GC
    }
  }

}

​ acquire()方法总结起来就是,

​ 如果线程获取锁成功,那么直接进入临界区,执行临界区的代码;

​ 如果当前线程如果获取锁失败,它被构建成一个Node加入到同步队列的的队尾。之后acquireQueued()方法中for(;;)代码块中不断的尝试获取锁和休眠。如果当前节点的prev节点是头节点,那么当前节点会尝试获取锁,直到成功,进入临界区。每个获取锁的节点释放锁了之后会被从同步队列中剔除(prev = next = waitStatus = thread = null)。如果最终失败,那么会从同步队列中取消当前线程的节点。

​ 再来看看release()方法,相对简单一些,相关代码如下:

public final boolean release(int arg) {
  // 首先执行子类中的tryRelease()方法,参照上面的ReentrantLock中的抽象静态类Sync的tryRelease()方法.
  if (tryRelease(arg)) {
   	// 如果释放锁成功,获取当前head节点,如果需要的话,唤醒下一个节点。
   	// head头结点的更新会在下一个线程获取锁的时候进行更新。
    Node h = head;
    if (h != null && h.waitStatus != 0)
      unparkSuccessor(h);
    return true;
  }
  return false;
}

总结起来,release()方法做的事情就是首先调用子类的tryRelease()(在ReentrantLock中就是不断减少AQS.state的值直至为0,最后将当前AQS对象的拥有锁线程设置为null ,返回true),然后如果条件满足的话唤醒下一个节点。

​ 最后,本篇文章到这里就结束了,本篇文章跟随ReentrantLock最基本的lock, unlock用法,阅读了AbstractQueuedSynchronizer同步器和ReentrantLock相关的源码,希望可以弄清我们平时写的代码中获取锁和释放锁的过程背后的逻辑到底如何。当然,并不是为了弄明白而弄明白,将这些代码逻辑弄明白的最后目的,还是为了在用到这块知识的时候能够写出健壮的代码,少掉到坑里。在遇到问题的时候,能够迅速的定位解决问题。

​ 下一篇,介绍ConditionObject加入使用的情况下的源码逻辑。

	下下篇,共享锁.....(魔幻的笑😁)

番外:自己花了不少时间来琢磨AQS中的代码,也看了不少相关的资料。又花了很多时间将它们整理到这篇文章,水平有限,文中如有错误之处还望多多指正,不然我误导了别人就不好了。AQS有一种奇特的魔力,搞的我最近都魔怔了,走路都在琢磨这里面的逻辑 T_T

参考资料:

  • Doug Lea大神自己写的相关介绍很好的帮助了我理解AQS:gee.cs.oswego.edu/dl/papers/a…

  • 《Java并发编程的艺术 The Art of Java Concurrency Programming》方腾飞 魏鹏 程晓明 这本书很细致的帮介绍了并发编程相关的底层知识和Java并发编程的相关知识点,在我看了N多相关的资料写了N多demo之后,这本书是真正把我从Java并发编程的巨坑里面捞起来的一本书,感谢

  • pages.cs.wisc.edu/~remzi/OSTE… OSTEP 感谢这本免费的在线书籍,让我0基础入门且省了大几十块钱^_^

​ 原文链接:mp.weixin.qq.com/s/9AfhFzCe3… 关注我的公众号 :青衣慕雪,一起学习一起探讨 ^_^