AQS和ReentrantLock

1 概述

哪里使用AQS?我们最常用的ReentrantLock类其实就是使用CAS和AQS来实现的。

ReentrantLock的构造方法中,sync对象其实就是继承了AbstractQueuedSynchronizer(AQS)。

private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
  ....
}
public ReentrantLock() {
    sync = new NonfairSync();
}

AQS翻译过来是,抽象队列同步器,其定义了一套多线程访问共享变量的同步器框架

内部类Sync就是自定义同步器,采用内部类的方式也是作者建议的。

AbstractQueuedSynchronizer的源码里作者还给了一个图。

  * 
  *      +------+  prev +-----+       +-----+
  * head |      | <---- |     | <---- |     |  tail
  *      +------+       +-----+       +-----+
  * 

画详细点

FIFO

2 state

state我们可以理解成为表示当前同步情况的一个状态变量。state是使用volatile修饰的一个变量,它的值代表锁的状态,state > 0表示锁被使用,state < 0表示锁被释放。

private volatile int state;
protected final int getState() {
    return state;
}
protected final void setState(int newState) {
    state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

getState用于获取state的值,比如一个线程调用该方法查看state的值是不是0,来判断当前是不是锁定的状态。

setState用来直接将state的值修改成其他,比如重入锁重入的时候将state的值+1。

compareAndSetState使用CAS的方式修改state的值,比如线程获取到锁尝试修改state的值为1。

state有两种共享方式:Exclusive(独占,只能有一个线程使用state)和 Share(共享,多个线程可以同时使用state)。

3 CLH同步队列

这里的CLH同步队列是一个FIFO(先进先出的)双向队列,元素的节点类型为Node,并且通过head和tail来记录队首和队尾的元素。Node节点对象用来维护需要获取锁的线程,当前一个节点释放锁的时候,该节点的线程就会被唤醒。

private transient volatile Node head;
private transient volatile Node tail;
static final class Node {
  static final Node SHARED = new Node();
  static final Node EXCLUSIVE = null;

  static final int CANCELLED =  1;
  static final int SIGNAL    = -1;
  static final int CONDITION = -2;
  static final int PROPAGATE = -3;

  volatile int waitStatus;

  volatile Node prev;

  volatile Node next;

  volatile Thread thread;

  Node nextWaiter;

  final boolean isShared() {
    return nextWaiter == SHARED;
  }

  final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
      throw new NullPointerException();
    else
      return p;
  }

  Node() {    // Used to establish initial head or SHARED marker
  }

  Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
  }

  Node(Thread thread, int waitStatus) { // Used by Condition
    this.waitStatus = waitStatus;
    this.thread = thread;
  }
}

说明一下几个重要的常量和变量:

  1. SHARED:表示被标记的线程是因为以共享的方式获取state失败而进入阻塞队列的;

  2. EXCLUSIV:表示被标记的线程是因为以独占的方式获取state失败而进入阻塞队列的;

  3. waitStatus:表示线程等待的状态

    static final int CANCELLED = 1; 表示线程因为中断或者等待超时,需要从等待队列中取消等待;
    static final int SIGNAL = -1; 锁被占用,队列中的head(仅仅代表头结点,里面没有存放线程引用)的后继结点node1处于等待状态,如果已占有锁的线程释放锁或被CANCEL之后就会通知这个结点node1去获取锁执行。
    static final int CONDITION = -2; 表示结点在等待队列中(这里指的是等待在某个lock的condition上,关于Condition的原理下面会写到),当持有锁的线程调用了Condition的signal()方法之后,结点会从该condition的等待队列转移到该lock的同步队列上,去竞争lock。(注意:这里的同步队列就是我们说的AQS维护的FIFO队列,等待队列则是每个condition关联的队列)
    static final int PROPAGATE = -3; 表示下一次共享状态获取将会传递给后继结点获取这个共享同步状态。

我们可以通过继承AbstractQueuedSynchronizer类来自己实现一个锁,使用的时候需要重写一些指定的方法(模板方法模式)。

//独占式的获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException();}
//独占式的释放同步状态,等待获取同步状态的线程可以有机会获取同步状态
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException();}
//共享式的获取同步状态
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException();}
//尝试将状态设置为以共享模式释放同步状态。 该方法总是由执行释放的线程调用。 
protected int tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
//当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
protected int isHeldExclusively(int arg) {  throw new UnsupportedOperationException();}

4 使用ReentrantLock分析

我们通过ReentrantLock来帮助了解一下加锁和释放锁时state和CLH队列的具体操作情况。

4.1 先来看lock方法

final void lock() {
  if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
  else
    acquire(1);
}

很好理解,state没有被占用的时候,当前线程使用锁,并将独占锁的线程持有者设置为自己。

否则,调用acquire方法。

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

第一个条件

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;
}

第一个条件总结来说就是再次尝试占用state,如果成功当前线程使用锁,如果失败并且当前线程和正在使用锁的线程是同一个,那么重入该锁。如果都不是,返回false。

第二个条件又分为几个方法,我们都看一下

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;
}
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;
            }
        }
    }
}
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);
    }
}
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;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

模拟一下,

  1. 假设线程A已经持有锁,并将state修改成1;
  2. 线程B通过lock方法进入到addWaiter,此时还没有维护队列,tail=nill;
  3. 进入enq方法的自旋中;
  4. 第一次循环 t = tail = null,进入if条件,head = tail = new Node();
  5. 第二次循环 t = tail = new Node(),进入else,将node(线程B)的上一个节点设置为t,将node设置为新的tail,将t的下一个节点设置为node,退出循环;
  6. 进入到acquireQueued方法,还是自旋,先获取node的上一个结点;
  7. 第一次循环,判断上一个节点是head并且当前线程B可以获取到state;
  8. 第7步通过,将head设置为node,返回false;
  9. 显然第7步不通过,进入下面的条件,先进入shouldParkAfterFailedAcquire方法;
  10. 判断上一个节点(pred)的waitStatus不是 -1,进入else,将上一个节点(pred)的waitStatus设置为 -1,返回false;
  11. 第二次循环,进入shouldParkAfterFailedAcquire方法,由于上一次循环修改waitStatus为-1,直接返回true;
  12. 再看parkAndCheckInterrupt方法,将线程B pack阻塞;
  13. 自旋并没有结束,只是被挂起了,这个自旋在后面还有用。

这时,队列是这样的

thread B

再来一个线程C,变成这样

thread C

4.2 再来看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;
}

首先我们能看到,重入锁在获取锁和释放锁的时候对state的操作参数都是1,这也就是为什么lock了几次就要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;
}
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);
}

我们还是分析一下代码

  1. 进入tryRelease方法,将state的值 -1 ,如果state = 0 就返回true,否则返回 false;
  2. 进入unparkSuccessor方法,先将head的waitStatus修改成0,在判断head的后续节点的waitStatus是不是 < 0 ;
  3. 小于 0 就唤醒,否则一直向下查找直到出现waitStatus小于 0 的节点,并且将它唤醒;

后面程序是怎么执行的呢?

  1. 回忆一下上面的lock逻辑,这时候线程B被唤醒了,继续自旋;
  2. 线程B获取state成功,就会从acquireQueued方法中退出,最后执行自己锁住的代码块中的程序。

你可能感兴趣的:(AQS和ReentrantLock)