通过ReentrantLock,看AQS的执行流程

131 阅读6分钟

thinkFirst

先回顾一下,在不显式依赖任何Lock包相关实现的情况下,如何做到线程安全?

  1. Synchronized

    • 用一个底层实现的monitor对象,作为竞争对象的锁实例。
    • 将monitor对象的地址(指针)信息记录在synchronized的对象头中
    • 一旦需要获取这把锁,就做竞争。轻量级锁就是CAS变更+自旋,重量级锁就涉及到内核态到用户态的切换
  2. volatile实现可见性与顺序性

  3. Atmoic包实现无锁编程

synchronized实现了线程安全性上的”全能“,通过实现原理,做出一个猜想:

通过外加状态值,对状态值进行竞争,是否可以做到在Java代码层面实现在JVM中Synchronized的效果?

这里回想一下,sync的功能特性:

  • 锁住代码,并且在锁结束前,不允许其他线程调用
  • 当然了,在sync中可以通过Thread.wait让出锁
  • 支持重入特性

第二步可以通过解锁后再加锁来解决。

我们知道:AQS提供了和sync类似的功能,那么这些特性应该都是具备的,带着这些准备,来看看AQS最常用的实现类:ReentrantLock的使用方法。

ReentrantLock的使用方法及其简单:

ReentrantLock lock = new ReentrantLock();
lock.lock();
//逻辑
lock.unlock();

那么就从这三个地方入手,来看看是具体是如何实现的。

构造方法

ReentrantLock有2个构造方法:

public ReentrantLock() {
    sync = new NonfairSync();
}
​
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

也就是说:会新建一个sync,默认为非公平,也可以显示指定为公平锁。

这俩都对应着内部的一个抽象类:Sync,而这个抽象类继承了AQS。

这些内部类并没有重写构造方法,因此使用的构造方法是AQS的,而AQS的就是个空方法,因此类里的属性,赋值都是默认值,state = 0.

private volatile int state;

lock

不管AQS我们先往下看:

lock.lock();

这里对应到的是

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

这里公平、非公平就体现出来了,两个上锁的方式是不同的:

  • fair:

    final void lock() {
        acquire(1);
    }
    
  • nonFair:

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

由于最后都是调用到了acquire方法,因此这里就是公平、非公平的不同之处了。

从外部来看,非公平锁,会先尝试CAS将state的值变成1,而公平锁就是直接acquire了,从语义上来说这里的acquire就是获取锁了。

acquire

那么接下来看看acquire方法:

//其实这里的arg,在实现里调用传入的都是1
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

从字面意义上来说,这里的流程的意思应该是:

  • 尝试获取锁,如果成功了就退出;
  • 获取锁失败了,那么就往获取队列中加入一个waiter,并让当前线程interrupt。

那么就按顺序来看看流程:

tryAcquire

这个是AQS留给子方法的hook,此处以ReentrantLock为实现类,看看实现方法:

  • fair:
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;
}

这里一看:其实也很简单,并没有循环的操作,那么说明获取锁的过程,都是只尝试一次的。

有几点发现:

  • 第一步获取c,如果c=0那么代表没有锁占用;后面如果当前线程和独占的线程相同那么state+1

    • 这里可以合理地进行推测:是否这里的state,代表的是当前线程的锁重入次数?
  • 这里会判断是否队列前有前驱,该部分代码如下:

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        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());
    }
    

    发现:

    • 这里没有加锁,那么是否会发生:t和h,在进入到这里之后发生了变更?

      • 其实这个并没有问题,因为下一步会尝试CAS修改;如果修改失败,那么就跳到下面了
    • 这里还有一个node的结构问题,这里能看到这里的node,应该是个链表结构,那么等到后面再来看看是如何进行维护的。

  • 如果当前线程是重入了,那么这里会有一个setState(nextc); 的操作,这里并没有任何并发保护;但state值是volatile的,那么这样子修改并没有太大的问题;如果希望在sync内部再次多线程sync,那么应该使用更多的锁来确保线程安全,这里也是相同的。

  • nonFair:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
​
//注意,这里是ReentarntLock中sync的方法,如果是tryLock,那么无论是否公平与否,都会调用这个
        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;
        }

细看代码,其实省去的是:

if (c == 0) {
    //省去了hasQueuePredecessor的这一步
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }

那么非公平锁就是当当前锁没有线程使用的时候就直接CAS变state值了,这里的含义就代表了会先去这么做,省去了判断排队的动作。

addwaiter,acquireQueued

上一步中,如果我们获取到了锁,那么就直接返回了;如果很不幸地,获取锁失败了,接下来就会尝试进入队列了。

在上一步tryAcquire中,我们可以猜测:AQS是通过队列的方式来维护获取的,那么根据名称也可以合理推测这里代表的是尝试入队。

addWaiter

那么第一步应该是先把节点搞出来,对应的就是addWaiter方法,如下:

//上一步传入的是node.exculsive
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;
}

不管语义上的问题(这里明明已经加到链表上了,那么应该是自己动起来了,为啥还要再acquireQueued?),先看看代码。

这里会先尝试将tail设置为当前节点并CAS设置这个tail,如果失败了或者tail为空就执行下面的enq方法。

  • 这里的注解上写的也很明确,先尝试快速入队。

enq方法干的事情和上面也很相似,不同的是如果CAS失败了就会死等了,直到设置成功了:

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

acquireQueued

解决了上面node的问题,接下来就看看这个方法了。

这里的代码是一个final方法,因此只要看AQS中的就可以了:

//这里的node是上面塞到队列最后的(至少塞完的时候是这样),arg就是acquire的传值,都是1
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);
    }
}

其中这块就不需要再说了,简单来说就是获得锁并CAS操作成功的情况下返回:

        if (p == head && tryAcquire(arg)) {
            setHead(node);
            p.next = null; // help GC
            failed = false;
            return interrupted;
        }

那么,如果拿不到锁就执行这块:

 if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;

这块的意思是,如果当前线程需要等待,那么就挂起(park)。这里就有一点sync为了避免轻量级锁过度使用CPU而让锁升级的味道在了。

shouldParkAfterFailedAcquire

看看这个方法:

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

这里就必须聊一下,AQS中node的几个状态了:

在前面的学习中我们知道了AQS会把线程封装成队列进行维护,队列中的抽象对象就是node。

这些node的状态值为waitStatus,有如下几种状态:

  • signal(-1)

    • 当前节点的前节点正在(或马上要)阻塞(通过park),因此当前节点必须unpark前节点,当前节点释放或取消。
    • 为避免竞争,获取的方法必须先判断是否需要信号,然后重试获取,失败则阻塞。
  • cancelled(1)

    • 当前节点因为超时或被中断而取消。
    • 节点并不会保持在这个状态。
    • 该状态的线程永远不会再阻塞了。
  • condition(-2)

    • 当前节点此时正在条件队列中(Condition queue)。

    • 不会被用作同步队列节点,直到当状态再转化后变成0为止。

      • 0并不代表任何此属性的其他用途,只是单纯简化机制。
  • propagate(-3)

    • 一个releaseShared 应当被传播到其他节点。
    • 这个值只会被设定在头节点上。
    • doReleaseShared中被设置,用于确保传播行为持续进行,即使因为中断,其他线程已经做完这个工作了。
  • 0

    • 非以上的所有情况。

那么这里就这么判断了:

  • 如果当前节点状态为signal了,那么当前线程需要挂起;
  • 如果不是,那么当前节点是否是取消状态?是的画就先把队列里取消的都清理掉;不是的话就把当前节点设置为signal。

这样子就可以知道:每次只有一个线程在CAS自旋请求锁其他的都park了,可以最大程度地降低CPU无意义地损耗。

unlock

那么这里入队获取,就大概清楚了;现在的问题变成了另外几个:

  • 既然只有一个线程自旋请求锁其他的都park了,那么其他的线程啥时候被唤醒?
  • 出队操作是怎么样的?

既然等待的线程,都是因为有其他线程目前已经占有锁了才等待,那么线程不等了的情况应该是在 持有锁的线程释放锁的时机被唤醒,来尝试获取被释放的锁。

因此我们就来看看这个释放锁对应的代码:

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

这里的releaseAQS搞成final了,那么就直接看看这里代码即可:

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

然后这里就有一个tryRelease的模板方法了,不过ReentrantLock里这个方法也是final,这里说明公平锁和非公平锁,在释放锁操作上是相同的。

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

这里的操作就相对简单了:

  • 这里的入参releases,代表的含义是在这里释放了几次锁(可重入性质相关的)。如果当前的state会变成0,那么就复位独占线程;否则,依然返回释放失败。

那么,如果这里的tryRelease成功了,且头节点不为空状态值不为0,那么就进入到下一步:

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

这里我们注意到,这里会把当前释放成功时的头节点,状态值置为0;

同时,如果后续还有线程,那么会尝试从末尾往前找到第一个处于等待状态的节点;找到之后,将这个节点唤醒。

总结

那么AQS主要的功能,就都大致了解完了,具体的流程可见: 能打开吗 processon.com/chart_image…

\