从AQS到ReentrantLock核心源码分析

从AQS到ReentrantLock核心源码分析

(一)概述

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。有了AQS我们可以实现自己的锁来满足我们自己独特的业务需求,下面是AQS的整体架构图:

在这里插入图片描述
上图展现了AQS的全貌,可以发现AQS还是非常复杂的。对于我们开发者来说,我们最需要关注的是其中的API层中的方法,因为这是我们自定义同步器需要重写的方法(当然不是每个都需要重写),因为我们需要写自己的逻辑来满足自己的需求,其他方法都是AQS已经帮我们写好的方法,可以直接使用。不过了解AQS的底层代码对于我们来说也是十分重要的,我在未来的文章中会对AQS的底层源码进行分析。这篇文章我们先聚焦到ReentrantLock这个常用的同步器。

(二)源代码概览

首先我先说一下AQS的基本原理,AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态。如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。到此为止,我们只需要知道AQS有一个被volatile修饰的变量state来保存当前的锁状态,还有一个队列用于给一些没抢到锁的线程等待,如下图所示:

在这里插入图片描述

我们先从宏观上来了解一下ReentrantLock,我们先来看ReentrantLock的两个构造方法:

private final Sync sync;

public ReentrantLock() {
    sync = new NonfairSync();
}

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

我们看到ReentrantLock有一个私有的Sync对象,这个对象就是表示这个ReentrantLock是公平的还是非公平的,构造方法就是按照我们的需要给他赋值。很显然ReentrantLock有两个构造方法,默认构造方法是将其设定为一个非公平锁。在有参构造方法里我们可以指定是否公平。众所周知,ReentrantLock是有公平锁和非公平锁这两种锁的,那么这两种锁必然对应两种不同的实现。下面我们来看构造方法中出现的两个类,NonfairSync和FairSync。这是两个内部类,我们可以看一下里面都有些什么内容:

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

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

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

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

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

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

很显然,这两个内部类都继承了Sync抽象类,我们可以看出这两个方法里面都有一个lock方法和tryAcquire方法,但是他们都是不一样的。这一点不难理解,因为公平锁和非公平锁的具体实现方式一定是不同的。如果你细心,你一定发现了上面的一写方法在文章开头那张大图中出现过,也就是说那些是AQS中的方法。构造函数调用完了,锁也就初始化完了,接下来就是我们熟悉的锁使用过程,最常用的就是lock和unlock方法,这两个方法的实现非常简单:

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

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

这样我们就把整体的逻辑梳理完了,到目前为止我们还没有提ReentrantLock是怎么实现的,我们现在主要是要将这个ReentrantLock的整体结构搞清楚。

(三)锁的实现

注意,ReentrantLock锁实现中大量用到了AQS中的方法,我们暂时不用知道这些方法是如何实现的(当然有的AQS方法不得不提一下,不然没法讲),我们只需要根据方法名猜出这个方法的功能,然后顺着逻辑往下读就行。公平锁和非公平锁的实现方式是不一样的,我们先讲公平锁。

1. 公平锁

在上面我们讲过,ReentrantLock的lock方法里面调用了FairSync对象的lock方法,lock方法又调用了AQS中的acquire方法,这个方法的功能是获得锁。下面我们看一下AQS中acquire方法的实现:

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

首先,调用tryAcquire方法尝试获得锁,若失败,则将其放入等待队列,若还失败,则执行selfInterrupt中断。逻辑很简单,但是有一个注意点,tryAcquire在AQS中只是定义了但未实现,也就是说尝试获得锁的方法需要由我们自己来实现。这里很容易理解,因为公平锁和非公平锁的尝试获得锁的方法肯定是不一样的,后面会作说明。下面我们来看ReentrantLock中的公平锁是如何实现tryAcquire方法的:

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

首先,我们发现这个方法有一个参数acquires,这个参数在FairSync对象的lock方法中传参为1,方法返回值为布尔类型,返回true表示成功获得锁,返回false表示获取锁失败,按照acquire的逻辑线程将被加入等待队列。然后我们来梳理主要逻辑,首先获得state,在ReentrantLock中,state被描述为获得锁的线程的数量。然后判断目前获得锁的线程数是不是为0,若state为0则执行hasQueuedPredecessors判断等待队列是不是有线程在等待队列中排队,这里就涉及到公平锁的一个特性——公平锁是不允许插队的,当线程到达时,如果发现等待队列中已经有别的线程在排队,必须也去排队,也可以认为公平锁是严格遵循先来先的服务原则(所以才称为公平)。如果没有线程排队,则用compareAndSetState方法尝试将state赋值为1,若成功则将持有锁的线程变为自己,也就是setExclusiveOwnerThread方法的功能,这样尝试获取锁的过程就完成了。

另一个分支,state不为0的情况,这里就涉及到ReentrantLock的另一个特性——可重入(可重入锁指的是在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁)。在这里我们要判断当前线程是不是已经持有锁的线程,也就是getExclusiveOwnerThread方法的含义,如果是则利用setState方法将state加一,代表获得锁的线程的数量加一。如果当前线程并不是已经持有锁的线程,则直接返回false。

加锁的逻辑看完了,我们来看解锁的逻辑,解锁方法定义在ReentrantLock类中,里面调用了AQS的release方法:

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

我们不难想象这个方法做了什么,上面我们提过,state表示获取锁的线程数量,那么释放锁一定就是将这个state进行减一操作了。我们来看源码是怎么写的,首先看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;
}

这是AQS中的方法,我们可以看到里面有个tryRelease方法,很明显这是需要ReentrantLock自己实现的(文章开头的大图上写了),我们直接看这个方法:

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

方法有一个参数release,很明显传参为1,返回值为布尔,代表释放释放锁成功。逻辑也非常简单,首先是用getExclusiveOwnerThread方法判断当前线程是否持有锁,众所周知没有持有锁的线程是不能释放锁的。如果是,则判断state - 1是不是为0,代表现在除了自己外持有锁的线程数是不是为0。如果为0则表示没有线程持有锁了,代表锁可以释放了,然后就可以调用setExclusiveOwnerThread方法将持有锁线程置空,返回true。如果state - 1不为0,则表示此锁被重入过,锁暂时还不能释放,返回false。

这样我们就把ReentrantLock的公平锁的部分全部分析完了,可以发现逻辑是非常简单的(ReentrantLock一共就只有几百行代码,大部分操作都是AQS帮我们写好的),非公平锁的逻辑和公平锁大部分是一样的,接下来看非公平锁。

2. 非公平锁

非公平锁大部分逻辑与公平锁一样,只有极其微小的区别。首先就是tryAcquire方法,因为非公平锁时运行插队的,所以和公平锁相比一定会少掉判断是否有线程在等待队列中等待的逻辑,我们来从头看一下看源码,首先是lock方法:

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

这里我们要注意:非公平锁的lock方法中的内容明显比公平锁要多(公平锁的lock方法中只有一个acquire方法),首先上来就是利用compareAndSetState尝试修改state,如果修改成功了直接就获得了锁,将持有锁的线程改为自己。否则进入acquire方法,与公平锁相同,acquire方法也会调用到非公平锁的tryAcquire方法,只不过实现与公平锁有一个很小的区别,下面我们看源码:

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

和公平锁不同,非公平锁没有直接在这里实现逻辑,而是又调用了父类中的一个方法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;
}

我们可以拿它与公平锁的tryAcquire方法作对比,发现居然只有一行的区别,就是少了一个判断是否有线程在等待队列等待的判断条件,这也就是公平锁和非公平锁最大的区别了。

非公平锁的解锁与公平锁是一样的,在此不再赘述,下面我们通过流程图以非公平锁为例来感受一下ReentrantLock与AQS之间的联系:

在这里插入图片描述

我们可以在图上找到刚刚我们在源代码中看见的所有逻辑,下面我们将图进行简化,来观察关系:

在这里插入图片描述

这个图中我们可以非常清晰的看出ReentrantLock和AQS是如何进行互相调用的,在此不得不惊叹AQS的独特魅力和设计的巧妙,有了AQS我们也可以写出自己的高性能并且能灵活满足业务逻辑需要的锁。

(四)小结

在这篇文章中主要了解了ReentrantLock的核心源码,当然没讲到的方法还有不少,但大多数都可以轻松看懂,我在这里只是将最重要的一部分拿出来讲了。事实上,讲ReentrantLock实际上是为讲AQS铺路,没有一个具体的锁是很难把AQS讲清楚的。我们先复习一下ReentrantLock的主要流程:

加锁:

  1. 通过ReentrantLock的加锁方法Lock进行加锁操作。
  2. 调用到内部类Sync的Lock方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,本质上都会执行AQS的Acquire方法。
  3. AQS的Acquire方法会执行tryAcquire方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire。
  4. tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。

解锁:

  1. 通过ReentrantLock的解锁方法Unlock进行解锁。
  2. Unlock会调用内部类Sync的Release方法,该方法继承于AQS。
  3. Release中会调用tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
  4. 释放成功后,所有处理由AQS框架完成,与自定义同步器无关。

在这里插入图片描述

在后面的文章中,我会再次利用ReentrantLock讲述AQS中的具体实现,AQS是面试的重点和难点之一,读懂源码是我们了解AQS实现最直截了当的方式。

2020年12月4日

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 鲸 设计师:meimeiellie 返回首页