Java ReentrantLock 源码阅读笔记(上)

Java ReentrantLock 源码阅读笔记(上)

Java 中的 ReentrantLockSynchronized 的性能在稍微新一点的虚拟机上的性能没有太大的区别,但是 ReentrantLock 的功能更加丰富,在实际编程中只要能够实现你的业务逻辑,我认为用哪个都没有什么区别。Synchronized 锁的实现是在虚拟机中实现的,ReentrantLock 中的绝大部分代码是用 Java 实现的,本篇文章内容就是来理解 ReentrantLock 的实现,当理解了 ReentrantLock 的实现后,其实 Synchronized 也就好理解了,我认为他们之间有太多的相似处。

获取锁

当线程需要获取锁的时候需要调用 ReentrantLock#lock() 方法:

Java 复制代码
public void lock() {
    sync.acquire(1);
}

这个 sync 变量有两个实现,分别是 FairSyncNoFairSync,他们分别表示公平锁和不公平锁,默认是使用不公平锁,关于公平锁和不公平锁我们后面讨论,这两个对象他们都是继承于 AbstractQueuedSynchronizer,也就是很多人口中的 AQSReentrantLock 的线程安全的实现都是基于它,其实我们阅读的代码大部分也是它。

这里我们看到获取锁的时候调用了 AbstractQueuedSynchronizer#acquire() 方法,同时传入了 1 作为参数。这里我还要再啰嗦下可重入锁和不可重入锁:可重入锁是当前线程获取锁后还可以再次获取锁,不过在释放锁的时候需要再调用同样次数的释放锁方法才可以完全释放锁;而不可重入锁在调用过获取锁方法后,就不能再次获取锁了,即使是同一个线程,需要释放上次的锁后才可以继续获取锁。ReentrantLock 是一种可重入锁,他的内部有一个状态来描述获取锁的次数,当没有获取锁时这个状态就是 0,调用一次获取锁的状态就会把这个状态加 1(也就是上面的 acquire 方法传入的参数),调用一次释放锁就会把这个状态减 1。

先来看看 AbstractQueuedSynchronizer#acquire() 方法的实现:

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

这里有三个重要的方法:
tryAcquire() 方法是一个抽象方法,等下我们看看 NoFairSync 的实现,他表示当前是否能够获取锁。
addWaiter() 方法是创建一个关于当前线程的 Node 并添加到队列尾部,这个队列就是等待锁的队列。
acquireQueued() 方法阻塞当前线程,直到当前线程获取到锁。

简单总结一下就是通过 tryAcquire() 方法判断是否能够获取锁,如果不能够获取通过 addWaiter() 方法将当前线程添加到等待队列的尾部,然后通过 acquireQueued() 方法来阻塞当前线程,直到当前线程获取到锁。

我们先来看看 NoFairSync#tryAcquire() 的实现:

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

@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取当前状态
    int c = getState();
    if (c == 0) {
        // 状态为 0 表示没有线程获取锁,通过 CAS 的方式修改状态,并把 owner 设置为当前线程,同时返回 true 表示成功获取到锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 表示同一个线程多次调用 lock 方法的情况,同样通过 CAS 的方式修改状态
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 其他线程已经获取锁,获取锁失败
    return false;
}

获取锁有三种情况:

  1. 当前没有锁:直接修改 state 为 1,并将当前线程设置为 owner,获取锁成功。
  2. 当前有锁但是锁是当前线程持有的(重入情况):将 state 加 1,获取锁成功。
  3. 其他线程持有锁,获取锁失败。

ReentrantLock 中大量使用了 CAS 的方式修改值,通过这样来保证值的修改是线程安全的,或者说在多线程编程中都有大量的使用,而且很多的 CPU 也有专门的 CAS 指令,大家不用太担心它的性能,我们也可以试着用 CAS 的方式来保证值的修改的安全,在 Java 中最简单的使用 CAS 的方式就是使用各种 Atomic 原子类。(又说废话了😂)

当获取锁失败后就会通过 AbstractQueuedSynchronizer#addWaiter() 在锁的等待队列的尾部添加当前线程的节点。

Java 复制代码
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}

修改队列也是用到了 CAS 的方式,当队列没有初始化,会创建一个空的 Node 同时赋值给 headtail

我们再继续看看 AbstractQueuedSynchronizer#acquireQueued() 方法是如何阻塞线程的,这个方法要注意理解:

Java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        // 死循环,直到获取锁成功或者线程中断才退出。 
        for (;;) {
            // 获取当前节点的前一个节点
            final Node p = node.predecessor();
            // 如果前一个节点是 head,调用 tryAcquire 方法尝试获取锁,如果获取成功,方法返回,同时将当前的 Node 设置为 head.
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // 判断是否要阻塞当前线程
            if (shouldParkAfterFailedAcquire(p, node))
                // 执行阻塞操作
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 判断前一个节点的等待状态,默认的等待状态是 0
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前一个状态是 SIGNAL 表示当前线程需要信号才能够继续,所以会执行阻塞,等待其他线程释放锁后发送信号才能继续
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        // 等待状态大于 0,表示已经取消。
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 这里表示等待状态为0,将其状态修改为 SIGNAL,然后进入下一次循环,下次循环如果不能够获取锁,线程就要被阻塞了。
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}


private final boolean parkAndCheckInterrupt() {
    // 阻塞操作,这是虚拟机 native 实现的,在 Linux 环境下,通常是 mutex + condition 实现的。
    LockSupport.park(this);
    return Thread.interrupted();
}

上面的方法可能没有那么好理解,可以先看看我对代码的注释,我这里举一个例子:假如有一个线程已经获取到锁,第二个线程去请求锁这个情况。

第二个线程去请求锁的时候,首先通过 tryAcquire() 方法去尝试获取锁,会返回 false;然后通过 addWaiter() 方法去添加一个尾部的节点,这个时候队列还没有初始化会添加一个空的节点同时指向 headtail;再然后调用 acquireQueued() 方法去等待锁的释放,这个方法是一个死循环,第一次循环首先判断前一个节点是否是 head,我们这种情况下就是 head,所以还会再次通过 tryAcquire() 方法去尝试获取锁,我们假定这次获取锁失败,然后进入 shouldParkAfterFailedAcquire() 方法去判断是否需要阻塞当前线程,由于 head 节点默认的 waitStatus 是 0,所以会被修改成 SIGNAL,同时进入下次循环,我们假定下次循环还是获取锁失败,然后又进入 shouldParkAfterFailedAcquire() 方法,这次由于 head 的状态被上次修改成 SIGNAL 了,所以会返回 true,然后会进入 parkAndCheckInterrupt() 方法完成当前线程的阻塞。我们假如过了一段时间第一个线程已经释放锁了,这时会释放当前线程的阻塞,然后进入第三次循环,我们假定这次获取锁成功,当前线程的 Node 就会被修改成 head,然后返回 lock() 方法,最终获取锁成功。

释放锁

我们直接看 unlock() 方法:

Java 复制代码
public void unlock() {
    sync.release(1);
}

我们继续看 AbstractQueuedSynchronizer#release() 方法:

Java 复制代码
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 中的实现:

Java 复制代码
@ReservedStackAccess
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;
}

其实就是和加锁相反的过程,当你理解了加锁的过程,上面的方法理解起来非常简单。

前面我们说到我们的 headwaitStatus 会被修改成 SIGNAL,然后我们会进入 unparkSuccessor() 去恢复被阻塞的线程。

Java 复制代码
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
     // 这个 node 就是 head。
    int ws = node.waitStatus;
    // 将 head 的 waitStatus 修改成 0
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
     // head 的下一个节点就是需要恢复的线程的节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
       // 这里需要移除掉被取消的节点
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        // 恢复对应节点的线程
        LockSupport.unpark(s.thread);
}

上面代码比较简单,我也写注释了,看起来应该比较轻松。

公平锁和不公平锁

我在前面分析了不公平锁的 tryAcquire() 方法的实现,我们再来看看公平锁的实现:

Java 复制代码
@ReservedStackAccess
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;
}

相对于不公平锁,添加了一个 hasQueuedPredecessors() 方法来判断队列中是否有等待的线程:

Java 复制代码
public final boolean hasQueuedPredecessors() {
    Node h, s;
    if ((h = head) != null) {
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null; // traverse in case of concurrent cancellation
            for (Node p = tail; p != h && p != null; p = p.prev) {
                if (p.waitStatus <= 0)
                    s = p;
            }
        }
        if (s != null && s.thread != Thread.currentThread())
            return true;
    }
    return false;
}

我们先想象一下不公平锁不添加队列的判断有什么问题,如果等待队列中有 10 个线程正在等待,第 11 个线程请求锁的时机和锁拥有的线程释放锁的时机相同,那么第 11 个线程就会和等待队列中的线程通过 CAS 的方式去竞争锁,如果第 11 个线程运气比较好它就会竞争过其他等待的 11 个线程而获取锁。人家都在排队,但是第 11 个线程却不用排队就可以获得锁,这显然是不公平的,所以怎么解决这种不公平现象呢?就是在获取锁的时候判断下是否有其他线程在等待,如果有,也得乖乖排队,这就显得公平了。

最后

本来想一次写完 ReentrantLock 源码阅读的文章,这样你看得累,我写得也累,所以 Condition 留到下篇再写,希望本篇文章对你有帮助。

相关推荐
七星静香7 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员7 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU8 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie611 分钟前
在IDEA中使用Git
java·git
Elaine20239126 分钟前
06 网络编程基础
java·网络
G丶AEOM28 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201329 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀30 分钟前
LRU缓存算法
java·算法·缓存
镰刀出海33 分钟前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel