AQS 万字图文全面解析

AQS 万字图文全面解析

前言

谈到并发,我们不得不说AQS(AbstractQueuedSynchronizer),所谓的AQS即是抽象的队列式的同步器,内部定义了很多锁相关的方法,我们熟知的ReentrantLockReentrantReadWriteLockCountDownLatchSemaphore等都是基于AQS来实现的。

我们先看下AQS相关的UML图:

AQS 万字图文全面解析_第1张图片

思维导图:

AQS 万字图文全面解析_第2张图片

AQS 实现原理

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

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

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

具体原理我们可以用一张图来简单概括:

AQS 万字图文全面解析_第3张图片

AQS 中提供了很多关于锁的实现方法,

  • getState():获取锁的标志 state 值

  • setState():设置锁的标志 state 值

  • tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回 true,失败则返回 false。

  • tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回 true,失败则返回 false。

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

目录结构

文章准备模拟多线程竞争锁、释放锁的场景来进行分析AQS源码:

三个线程(线程一、线程二、线程三)同时来加锁/释放锁

目录如下:

  • 线程一加锁成功时AQS内部实现

  • 线程二/三加锁失败时AQS中等待队列的数据模型

  • 线程一释放锁及线程二获取锁实现原理

  • 通过线程场景来讲解公平锁具体实现原理

  • 通过线程场景来讲解 Condition 中 await()signal()实现原理

这里会通过画图来分析每个线程加锁、释放锁后AQS内部的数据结构和实现原理

场景分析

线程一加锁成功

如果同时有三个线程并发抢占锁,此时线程一抢占锁成功,线程二线程三抢占锁失败,具体执行流程如下:

AQS 万字图文全面解析_第4张图片

此时AQS内部数据为:

AQS 万字图文全面解析_第5张图片

线程二线程三加锁失败:

AQS 万字图文全面解析_第6张图片

有图可以看出,等待队列中的节点Node是一个双向链表,这里SIGNALNodewaitStatus属性,Node中还有一个nextWaiter属性,这个并未在图中画出来,这个到后面Condition会具体讲解的。

具体看下抢占锁代码实现:

java.util.concurrent.locks.ReentrantLock .NonfairSync:
static final class NonfairSync extends Sync {

    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) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

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

线程二抢占锁失败

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

AQS 万字图文全面解析_第7张图片

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

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();
    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)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

nonfairTryAcquire()方法中首先会获取state的值,如果不为 0 则说明当前对象的锁已经被其他线程所占有,接着判断占有锁的线程是否为当前线程,如果是则累加state值,这就是可重入锁的具体实现,累加state值,释放锁的时候也要依次递减state值。

如果state为 0,则执行CAS操作,尝试更新state值为 1,如果更新成功则代表当前线程加锁成功。

线程二为例,因为线程一已经将state修改为 1,所以线程二通过CAS修改state的值不会成功。加锁失败。

线程二执行tryAcquire()后会返回 false,接着执行addWaiter(Node.EXCLUSIVE)逻辑,将自己加入到一个FIFO等待队列中&#x

你可能感兴趣的:(java,服务器,linux)