【洞悉AQS】通过ReentrantLock一步一图彻底了解AQS实现原理

前言

谈到并发,我们不得不说AQS(AbstractQueuedSynchronizer),所谓的AQS即是抽象的队列式的同步器,内部定义了很多锁相关的方法,例如:

  • getState():获取锁的标志state值
  • setState():设置锁的标志state值
  • tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回true,失败则返回false。

这里还有更多的方法并没有列出来,我们以ReentrantLock作为突破点通过源码和画图的形式一步步了解AQS内部实现原理。

image.png

目录结构

文章准备模拟场景来进行解析:

三个线程(线程一、线程二、线程三)同时来加锁/释放锁,然后通过代码和画图一步步解析其中的实现。

目录如下:

  • 线程一加锁成功时AQS内部实现
  • 线程二/三加锁失败时AQS中等待队列的数据模型
  • 线程一释放锁及线程二获取锁实现原理

这里会分析每个线程加锁、释放锁内部的一系列实现原理

AQS实现原理

AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,然后线程放入一个FIFO的等待队列中使用UNSAFE.park()来挂起当前线程。

另外state的操作都是使用CAS来保证其并发修改的安全性。

具体原理我们可以用一张图来简单概括:(此图片来源:https://www.cnblogs.com/waterystone/p/4920797.html)

image.pngimage.png

场景分析

线程一加锁成功

如果同时有三个线程并发抢占锁,此时线程一抢占锁成功:

image.png

此时线程二线程三加锁失败:

image.png

抢占锁代码实现:

java.util.concurrent.locks.ReentrantLock .NonfairSync:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

这里使用的ReentrantLock非公平锁,线程进来直接利用CAS尝试抢占锁,如果抢占成功则state被改为1,然后设置独占锁线程为当前线程。如下所示:

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

线程二抢占锁失败

我们按照真实场景来分析,此时线程一抢占锁成功,state变为1,所以线程二通过CAS修改state变量必然会失败。此时AQSFIFO队列中数据如图所示:

image.png

我们将线程二执行的逻辑一步步拆解来看:

java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire():

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

先看看tryAcquire()的具体实现:
java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire():

final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state状态,线程一加锁成功了,此时state=1
    int c = getState();
    // 如果state=0,说明可以尝试利用CAS进行加锁操作
    if (c == 0) {
        // 加锁成功的逻辑
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程和独占锁线程是同一个,那么可以重入加锁
    // 重入加锁后,state=2
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 返回false加锁失败
    return false;
}

这段代码走下来,此时全局变量state=1,所以通过CAS修改state的值不会成功。

而此时持有锁的线程是线程一,所以线程二*也不满足重入的条件。

线程二执行tryAcquire()后返回false,接着执行addWaiter(Node.EXCLUSIVE),代码实现如下:

java.util.concurrent.locks.AbstractQueuedSynchronizer.addWaiter():

private Node addWaiter(Node mode) {
    // mode = Node EXCLUSIVE = null
    // 创建一个新的node,thread = 线程二,nextWaiter = null
    Node node = new Node(Thread.currentThread(), mode);
    // 此时tail = null,直接执行enq操作
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 线程二直接进入队列操作
    enq(node);
    return node;
}

此时tail指针为空,直接调用enq(node)将当前线程加入等待队列尾部:

private Node enq(final Node node) {
    for (;;) {
        // 第一次循环tail = null
        Node t = tail;
        if (t == null) {
            // 用CAS将head设置为一个新Node
            if (compareAndSetHead(new Node()))
                // tail 和 head都指向这个新Node
                tail = head;
        } else {
            // 第二次循环进入,node的前置节点设置为tail = head
            node.prev = t;
            // 用CAS操作设置tail节点为当前传入的node节点
            if (compareAndSetTail(t, node)) {
                // t开始时指向tail=head节点,通过CAS将tail指向node后
                // 设置t.next=head.next=node
                t.next = node;
                // 返回t=head节点
                return t;
            }
        }
    }
}

第一遍循环tail指针为空,进入if逻辑中,此时队列中数据:

image.png

执行完成之后,headtailt都指向第一个元素(new Node())

接着执行第二遍循环,进入else逻辑,此时已经有了head节点,这里要操作的就是将线程二这个Node节点挂到head节点上来。

addWaiter()方法执行完后会返回当前插入线程二构建的Node节点,此时队列中的数据为:

image.png

再接着看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
执行逻辑,此时传入的为线程二构建的Node信息:

java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued():

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // p为线程二Node的前置节点,也就是head节点
            final Node p = node.predecessor();
            // p= head成立,继续使用CAS尝试加锁,此时线程一还在持有锁
            // state = 1,所以加锁失败
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 执行shouldParkAfterFailedAcquire方法
            // shouldParkAfterFailedAcquire方法中将head节点的waitStatus变为了SIGNAL=-1
            // 接着执行parkAndChecknIterrupt,调用LockSupport.park()
            // 挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndChecknIterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // ws为head节点的waitStatus=null
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 通过CAS操作,将head节点的waitStatus变为SIGNAL=-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    // 调用底层的park方法,挂起当前线程
    LockSupport.park(this);
    return Thread.interrupted();
}

acquireQueued()这个方法会先判断当前传入的Node对应的前置节点是否为head,如果是则尝试加锁。加锁成功过则将当前节点设置为head节点,然后空置之前的head节点。

如果加锁失败或者Node的前置节点不是head节点,首先将Node的前置节点中的waitStatus设置为SIGNAL(值为-1),
然后挂起当前Node节点(当前Node线程二创建的节点),操作后AQS队列中的数据如下图:

image.png

此时线程二就静静的待在AQS的等待队列里面了,等着其他线程释放锁来唤醒挂起的线程。

线程三抢占锁失败

看完了线程二抢占锁失败的分析,那么再来分析线程三抢占锁失败就很简单了,先看看addWaiter(Node mode)方法:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // pred = tail = 线程二
    Node pred = tail;
    // 此时pred不为空
    if (pred != null) {
        // 设置线程三的前置节点为线程二
        node.prev = pred;
        // 使用CAS将线程三设置为tail节点
        if (compareAndSetTail(pred, node)) {
            // pred = 线程二的next节点设置为线程三
            pred.next = node;
            // 返回线程三节点
            return node;
        }
    }
    enq(node);
    return node;
}

执行完后AQS中队列数据如图:

image.png

接着执行acquireQueued()方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // p = 线程三的前置节点= 线程二
            final Node p = node.predecessor();
            // 判断线程二是否是head节点,如果是则尝试抢占锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 执行shouldParkAfterFailedAcquire方法
            // 将线程三的前置节点线程二中的waitStatus变为SIGNAL = -1
            // 然后执行parkAndCheckInterrupt将线程三也挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

执行完后AQS中队列数据如图:

image.png

线程一释放锁

现在来分析下释放锁的过程,首先是线程一释放锁,释放锁后会唤醒head节点的后置节点,也就是我们现在的线程二,执行完后AQS队列数据如下:

image.png

此时线程二已经被唤醒,继续尝试获取锁,如果获取锁失败,则会继续被挂起。如果获取锁成功,则AQS中数据如图:

image.png

接着还是一步步拆解来看,先看看线程一释放锁的代码:

java.util.concurrent.locks.AbstractQueuedSynchronizer.release()

public final boolean release(int arg) {
    // tryRelease() 方法实现在ReentrantLock中实现的
    if (tryRelease(arg)) {
        // 如果释放锁成功,定义h=head节点
        Node h = head;
        // 如果head不为空,此时head.waitStatus=SIGNAL=-1
        if (h != null && h.waitStatus != 0)
            // 执行唤醒操作,唤醒之前挂起的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

此时看ReentrantLock.tryRelease()中的具体实现:

protected final boolean tryRelease(int releases) {
    // getState()是获取state变量,此时state=1
    // releases传入进来的也是1,所以c=0
    int c = getState() - releases;
    // 如果释放锁的线程,不是当前独占锁线程,直接抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果c=0则说明释放锁成功
    if (c == 0) {
        free = true;
        // 设置独占锁线程为null
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

执行完ReentrantLock.tryRelease()后,state被设置成0,Lock对象的独占锁被设置为null。可以看下执行后AQS中的数据:

image.png

接着执行java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()方法,唤醒head的后置节点:

private void unparkSuccessor(Node node) {
    // node为head线程,此时node.waitStatus=SIGNAL=-1
    int ws = node.waitStatus;
    if (ws < 0)
        // 设置node节点的waitStatus为0
        compareAndSetWaitStatus(node, ws, 0);

    // s=node.next=head.next,获取线程二Node节点
    Node s = node.next;
    // 此时s.waitStatus=SIGNAL=-1 条件不成立
    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;
    }
    // 执行LockSupport.unpark,这里是唤醒线程二
    if (s != null)
        LockSupport.unpark(s.thread);
}

逻辑如图:

image.png

此时线程二被唤醒,线程二接着之前被park的地方继续执行,继续执行acquireQueued()方法。

线程二唤醒继续加锁

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取线程二的前置节点为head
            final Node p = node.predecessor();
            // p=head,然后尝试加锁,对state进行CAS操作
            // 此时仍有可能加锁失败,因为我们使用的ReentrantLock中的非公平锁
            // 如果真好有新的线程进来挣钱锁,线程二就有可能加锁失败
            // 如果加锁失败就还会被挂起
            if (p == head && tryAcquire(arg)) {
                // 线程二加锁成功,将线程二设置为head节点
                setHead(node);
                // 设置p.next=head.next为null,方便GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

执行完后线程二获取锁,而线程二也变为head节点,之前的head节点被空置,等着被垃圾回收,此时AQS中队列数据如下:

image.png

此时 线程二获取锁成功,并且之前的head节点会被垃圾回收掉。

线程二释放锁/线程三加锁

线程二释放锁时,会唤醒被挂起的线程三,流程和上面大致相同,被唤醒的线程三会再次尝试加锁,具体代码就不再分析了,此时AQS中队列数据如图:

image.png

总结

这里用了一步一图的方式来展示了ReentrantLock的实现方式,而ReentrantLock底层就是基于AQS实现的,所以我们也对AQS有了深刻的理解。

由于篇幅原因,还有很多细节没有讲到,比如ReentrantLock公平锁的实现,可重入锁的实现,Condition的实现等,当然大家可以依照着我这种分析模式去尝试一步步画图解析,相信这些细节也都能一步步破解。

后面我还会介绍ReentrantReadWriteLock
的实现原理,仍然使用一步一图的模式来讲解,敬请期待。

posted @ 2020-04-30 08:15  一枝花算不算浪漫  阅读(266)  评论(0编辑  收藏  举报