ReentrantLock非公平锁源码分析

ReentrantLock源码分析

RenntrantLock是一种独占锁,即是:有且仅有一个线程可以获得该锁,若有一个线程获得该锁,其他线程必须等待;

1.先从其中的非公平锁谈起

那就先说明其加锁的整个过程吧,加锁的流程图如下:

ReentrantLock.jpg

具体加锁的整个调用的方法如上图所示,实现的源代码会做详细的说明 说明之前我先对ReentrantLock类的整个结构进行必要的说明,如下图所示:

reentrantlock结构.jpg

其中ReentrantLock实现了Lock和Serializable接口,其中Sync、NonfairSync和FairSync是其内部类,其中Sync继承了AbstractQueuedSynchronizer(AQS)类,非公平锁(NonfairSync)和公平锁(fairSync)都继承了Sync类,由于java语言本身的特点,即间接继承了AQS类,此类中有较多的方法,由子类重写,实现非公平锁(NonfairSync)和公平锁(fairSync)的加锁和解锁过程。

加锁实际上其实很简单,如下所示:

ReentrantLock lock = new ReentrantLock();  // 未传入参数默认使用的是非公平锁
lock.lock();  
复制代码

但是,其实际是如何完成加锁的呢?听我细细道来:

正如下图所示,整个函数的调用过程,完成了加锁的整个流程:(允许我把上个图在拿下来,防止回过头去看,多不方便,是吧!!!)

ReentrantLock.jpg

好了,现在进入源码分析:

// lock.lock()调用的方法实际调用了其内部类的Sync的lock()方法,进行加锁
public void lock() {
    sync.lock();
}
复制代码

点开lock()方法,我们可以看到如下所示的一个抽象方法(在Sync类内部):

abstract void lock();
复制代码

该方法在Sync内部交由其子类非公平锁(NonfairLock)和公平锁(fairLock)重写; 那么我们进入NonfairSync类内部,可以看到如下代码:

final void lock() {
    // 首先执行CAS,判断设置state状态是否成功,设置成功,则将当前线程获得独占锁锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else  // 负责执行acquire(1)语句,接着向下看
        acquire(1);
}
复制代码
// 该方法有两个作用:
// 1.线程首先会调用tryAcquire()方法尝试获取锁;
// 2.将未获取到锁的线程挂起,等待被唤醒;
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
复制代码

其中,tryAcquire(arg)完成作用1,acquireQueued(addWaiter(Node.EXCLUSIVE), arg))完成作用2;

接着进入tryAcquire(arg)源码分析,发现点开一看,如下所示,又调用了一个方法,不怕,继续向下找,总能找到想要看的内容:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
复制代码

nonfairTryAcquire(acquires)源码就是这样:

final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    int c = getState();  // 获取state变量的值
    if (c == 0) {  // 判断state是否为0,若成立,则进入if语句
        if (compareAndSetState(0, acquires)) { // 执行CAS操作,设置state变量为1,即:当前线程获得同步状态(锁)
            setExclusiveOwnerThread(current);  // 将当前线程设置为独占式
            return true;
        }
    }
    // 判断当前线程是否是当前正在执行的线程,若是,则执行
    // 此处就可以理解为什么ReentrantLock是重入锁了。
    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;  // 若以上条件都不成立,则返回false,得到此返回的结果,就会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,下面会对此分析;
}
复制代码

好的进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)该方法体内,首先执行的是addWaiter(Node.EXCLUSIVE)方法,源码如下:

private Node addWaiter(Node mode) {
    // 首先,将当前线程,封装为Node节点,其Node类是AQS的内部类
    Node node = new Node(Thread.currentThread(), mode);
    // 指向尾结点
    Node pred = tail;
    if (pred != null) {  // 若尾结点不为空
        node.prev = pred;  // 获得前驱节点
        if (compareAndSetTail(pred, node)) {  // 利用CAS将当前插入到尾部
            pred.next = node;
            return node;
        }
    }
    enq(node);  // 第二个if条件不成立,执行此方法,依然是将节点入队
    return node;  // 返回当前节点,进入acquireQueued()方法
复制代码
private Node enq(final Node node) {
    // 死循环
    for (;;) {
        Node t = tail;  // 获取尾部节点
        if (t == null) { // 若尾结点为空
            if (compareAndSetHead(new Node()))  // 执行CAS常见一个新节点作为头结点,并且创立成功,则将尾结点指向头结点
                tail = head;
        } else {  // 存在尾结点
            node.prev = t;
            if (compareAndSetTail(t, node)) {  // 利用CAS将传入的节点添加到尾部
                t.next = node;
                return t;  // 返回当前节点的前驱节点
            }
        }
    }
}
复制代码

线程的挂起,以及线程被唤醒后尝试获取锁的代码关键就在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;
            }
            // 从字面意思就可以看出,获取锁失败应当挂起线程,当然挂起线程是parkAndCheckInterrupt()方法执行
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

接着在看shouldParkAfterFailedAcquire(p, node)方法与parkAndCheckInterrupt()方法是如何挂起线程的。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获得当前节点的前驱节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)  // 若当前节点为待唤醒状态,则返回true,
        return true;
    if (ws > 0) {  // 若当前状态为CANCELED状态
        do {
            node.prev = pred = pred.prev; 
        } while (pred.waitStatus > 0);  // 寻找状态为小于0的状态(SIGNAL或CONDITION或PROPAGATE(RentrantLock中暂时不考虑))
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  // 设置前驱节点为SIGNAL状态
    }
    return false;
}
复制代码

parkAndCheckInterrupt()方法如下:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);  // 挂起当前线程
    return Thread.interrupted();
}
复制代码

至此,整个加锁的过程就结束了,可能有些人比较迷惑,同步队列是如何构造的?解锁后,线程又是如何获取锁的?这些留待下篇文章进行分析^_^。

猜你喜欢

转载自juejin.im/post/7037320021361721351