独占锁下的AQS

99 阅读5分钟

从ReentrantLock的lock方法开始

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

当我们调用ReenTrantLocklock方法时,内部调用了Sync内部类的lock方法,让我们看一看Sync

abstract static class Sync extends AbstractQueuedSynchronizer {

    abstract void lock();
}

可见Sync是一个抽象类,lock方法由具体的子类来实现,我们都知道ReentrantLock支持公平锁和非公平锁,所以ReentrantLock内部有两种lock方法的实现!我们这里从公平锁入手

FairSync公平锁

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        // ...省略
    }
}

FairSync类的lock方法中,调用了acquire()方法,但是FairSync类中仅有两个方法,并没有看到acquire方法的身影,当我们在idea中按住ctrl点击此方法时,我们就进入了AbstractQueuedSynchronizer类中!

AbstractQueuedSynchronizer类初识

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

acquire方法调用了一个tryAcquire方法,此方法是抽象方法,在FairSync类中对其进行了实现,如果tryAcquire方法返回true,代表抢占锁成功,否则失败!我们先大概过一下这个方法的逻辑

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                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;
    }
  1. 获取到当前线程
  2. getState方法是在AbstractQueuedSynchronizer类中,返回此类中的state属性
    • 如果state的值为0,代表此时锁没有被线程抢占
    • 如果state大于0,代表锁被线程抢占了
  3. 当state为0时,代表此时锁没有被线程抢占,hasQueuedPredecessors()方法是判断是否有线程在等待锁,由于是公平锁,如果已经有线程在等待,那么当前线程就不能获取锁,具体逻辑后面补充;如果当前没有线程等待锁,就使用compareAndSetState(0, acquires)设置state的值,如果设置成功,当前线程就抢到了锁,那么就使用setExclusiveOwnerThread(current)方法来记录那个线程抢到了锁,此方法在AbstractQueuedSynchronizer类的父类AbstractOwnableSynchronizer中,此类的方法逻辑很简单,就不多叙述了
    private transient Thread exclusiveOwnerThread;
    
    protected final void setExclusiveOwnerThread(Thread thread) {
       exclusiveOwnerThread = thread;
    }
    
    protected final Thread getExclusiveOwnerThread() {
       return exclusiveOwnerThread;
    }
    
  4. 如果state大于0,判断当前线程是不是锁的持有者,如果是就增加state的值,这里体现了ReentrantLock的可重入性
  5. 如果以上的情况都不满足,证明获取锁失败,返回false

AbstractOwnableSynchronizer类中的Node节点

让我们回到AbstractOwnableSynchronizer的acquire方法中

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

在分析方法之前,我们需要先了解AbstractOwnableSynchronizer类中的Node类

static final class Node {

    static final Node EXCLUSIVE = null;

    static final int SIGNAL    = -1;
    
    static final int CANCELLED =  1;
    
    volatile int waitStatus;

    volatile Node prev;

    volatile Node next;

    volatile Thread thread;

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

这里只列出了Node支持独占锁部分的属性,下面逐个解释

  • EXCLUSIVE属性代表当前节点所指向的线程是否是独占锁模式
  • SIGNAL的值为-1,代表当前线程是在释放锁时有义务去唤醒同步队列中阻塞的线程
  • CANCELLED的值为1,代表当前线程取消获取锁
  • waitStatus代表Node的状态,初始值为0
  • prevnextNode的前后指针
  • threadNode代表的阻塞线程

AbstractOwnableSynchronizer类中维护了一个双向同步队列,上方分析的Node就是队列中的节点,需要注意的是这个队列的头结点是空节点,或者说是当前占据锁的线程代表的节点。 是时候分析AbstractOwnableSynchronizer类中的精髓了,当线程占用锁失败后,会被addWaiter方法加入到同步队列中

if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

addWaiter 将线程加入到同步队列中


private transient volatile Node head;

private transient volatile Node tail;

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;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return 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)) {
                t.next = node;
                return t;
            }
        }
    }
}

  • 新建node节点,设置模式为Node.EXCLUSIVE模式,传入当前线程,需要注意的是waitStatus没有被初始化,所以waitStatus的值是0

  • tail节点就是双向同步队列的尾结点

    • 如果tail节点不为null,证明队列已经被初始化了,进入if语句
      • 将新建节点的prev指针指向tail,然后使用cas设置新建节点为tail节点,如果设置成功,使old tail节点的next指针指向新建节点,此时新的tail节点出现了
  • 如果队列没有被初始化或者是cas设置新的node为tail节点失败了,进入到enq方法

  • enq方法是一个自旋操作

    • 如果tail节点为null,就初始化队列,新建一个空节点作为队列的head,将tail指向head;需要注意的是head节点是一个没有数据的节点,并且在设置的时候使用了cas,保证队列只被初始化一次

    • 初始化了队列之后继续循环,跟addWaiter方法的逻辑一样,使用cas设置新的tail节点;这里有一个需要注意的细节,当cas设置node为新的tail节点以后,当前线程有可能来不及设置old tail节点的next指针指向新tail

      image.png

独占锁的核心acquireQueued方法

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 {
        if (failed)
            cancelAcquire(node);
    }
}

node加入到同步队列之后,就进入了acquireQueued方法,这个方法也是一个自旋方法

  • node.predecessor()获取到node的前一个节点
  • 如果node的前一个节点是head节点,那么就尝试去获取锁;如果说node的前一个节点是head节点,证明node是同步队列中的第一个被阻塞的节点,在node加入队列的过程中,持有锁的线程有可能已经释放锁,所以这里node才会去尝试获取锁;获取锁成功后,设置node为头结点,所以head节点也可以看成当前获取到锁的线程,只是headthread属性为空
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
  • 如果上方的条件不满足或者尝试失败,就会进入shouldParkAfterFailedAcquire(p, node)方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  • 这里需要详细解释一下SIGNAL的含义,SIGNAL就相当于一个闹钟,如果某一个线程的状态是SIGNAL,那么在这个线程释放锁的时候有义务去唤醒同步队列中阻塞的线程,if (ws == Node.SIGNAL)这个条件为真的话,代表node的闹钟已经设置了,在前一个节点释放锁的时候一定会来唤醒我,那我只管睡觉等待就好了。,最终shouldParkAfterFailedAcquire方法返回true,就会将当前线程阻塞。
  • if (ws > 0) 代表CANCELED状态,证明前一个节点所代表的线程已经放弃获取锁了,那就进行清除处理,将同步队列中所有状态为CANCELED状态的节点清除
  • else的情况就是前一个节点的状态为0,那么我们就帮前一个线程设置下状态为SIGNAL,为自己设置好闹钟,免得在前一个线程释放锁之后我还没睡醒,最后返回false去重新尝试获取锁

最后就是parkAndCheckInterrupt()方法,此方法就是将线程阻塞;在线程苏醒之后并且返回线程的阻塞标记并且重置阻塞标记

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

返回线程阻塞标记的原因是线程在阻塞的过程中有可能被调用interrupt方法,但是此时线程不能及时响应,那么我们应该给他记录下来,当线程获取退出循环后,在进行处理;

这里说一下什么时候节点的状态会进入到CANCELED状态

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 {
    if (failed)
        cancelAcquire(node);
}

在上方的代码中,如果循环中断,并且failedtrue那么就会设置节点的状态为CANCELED,这种情况只有tryAcquire方法抛出异常才会出现!如果是ReentrantLock的话只有在重入次数太多才会导致这种现象

线程释放锁

public void unlock() {
    sync.release(1);
}

线程释放锁是调用了AbstractOwnableSynchronizer类的release方法

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

AbstractOwnableSynchronizer类会先调用子类的tryRelease方法,在Sync类中提供了具体实现

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
  • 获取到state,然后减去1
  • 如果当前线程不是获取到锁的线程,就抛出异常
  • 如果state的值为0了(因为锁是可重入的),可以释放锁,设置锁的持有线程为null
  • 返回free

下面的逻辑才是重点

Node h = head;
if (h != null && h.waitStatus != 0)
    unparkSuccessor(h);
return true;

h != nullhead不为null,证明队列中有阻塞的线程

h.waitStatus != 0 这个条件很有意思,我们需要先思考什么时候h.waitStatus等于0,什么时候h.waitStatus != 0

  • 在初始化的时候 h.waitStatus=0
  • shouldParkAfterFailedAcquire方法中,后继节点会设置前面节点的waitStatusSIGNAL,此时h.waitStatus!=0

所以说如果h.waitStatus=0,证明h的后继节点没有来的急设置h的waitStatus状态,证明后继节点所代表的线程还在运行,就不用去唤醒了。

这也是为什么后继节点在shouldParkAfterFailedAcquire方法把前一个节点的waitStatus设置为SIGNAL后会继续尝试获取锁。因为前面的节点有可能正在执行release方法,并且在后继节点还没有来的急设置状态的时候执行完毕了release方法,如果后继节点直接沉睡,那么就永远不会被唤醒

如果h.waitStatus != 0,那么后继节点所代表的的线程很有可能被阻塞了,需要去唤醒

下面看一下unparkSuccessor的逻辑

private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}
  • 如果头结点的waitStatus小于0,设置waitStatus为0
  • 然后从同步节点的最后一个节点开始寻找最靠近头结点并且waitStatus小于等于0的节点
    • 为什么从最后一个节点开始而不是从第一个节点开始呢?其实上面已经埋下了伏笔,在addWaiter的时候,新的tail节点有可能还没有被old tail节点的next指针连接,所以如果从前往后遍历的话就会少遍历一个节点
  • 在获取到节点后直接去唤醒

hasQueuedPredecessors方法

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • 如果h != t && (s = h.next) == null时,证明addWaiter方法被调用,但是cas设置新的tail成功了,还没有成功设置tail.prev=h,不管怎么说,此时队列中是有节点的,返回true
  • 如果h != t && s.thread != Thread.currentThread()时,证明第一个队列中的第一个有效节点不是当前线程,那么返回true;也就是说,如果当前获取锁的线程是队列中的第一个有效节点(头结点无效的),那么这个线程有获取锁的资格

推荐一篇写的非常好的文章:segmentfault.com/a/119000001…