Java学习笔记(多线程):ReentrantLock 源码分析

发布于:2025-04-09 ⋅ 阅读:(35) ⋅ 点赞:(0)

本文是自己的学习笔记,主要参考资料如下
JavaSE文档


1、AQS 概述

1.1、锁的原理

AQS是指抽象类AbstractQueuedSynchronizer。这个抽象类代表着一种实现并发的方式。

具体实现方式是使用volitile修饰state变量,保证了state的可见性和有序性。最后使用CAS改变state的值,保证原子性。

那么AbstractQueuedSynchronizer通过更新state的值来实现的加锁和解锁。

下面是关键源代码的截图。
请添加图片描述
请添加图片描述


1.2、任务队列

AQS中维护了一个任务队列,是一个双向队列。队列节点是内部类Node

Node中记录者节点的状态waitStatus,比如CANCELSIGNAL等分别表示该任务节点已经取消和任务节点正在沉睡需要被唤醒。

当然,因为是双向列表所以也有指向前后节点的指针。下面是Node源码的部分截图。
请添加图片描述

这个队列会初始化一个头结点和一个尾结点作为虚拟节点。头结点的状态在整个加锁和释放锁的过程中都会变化。

1.2.1、结点的状态变化

当头结点指向的Node才拥有锁。

这里主要介绍三个状态

  • 0, 表示当前Node后续无节点在排队。不表明是否拥有锁。
  • -1,表示除了当前Node在排队以外,还有其他Node排在当前Node后面。不表明是否拥有锁。
  • 1,表示当前Node可能因为等待时间太长而放弃获取锁。

下面是三个Node在队列中的状态。这里从左到右解释他们的状态。
请添加图片描述
head指向第一个Node,所以当前Node拥有锁。

第一个NodewaitStatus=-1表示后续有节点等待获取锁。当该节点释放锁时会唤醒后续的节点。

第二个NodewaitStatus = -1,后续有节点等待获取锁。

第三个NodewaitStatus = 0,后续无节点等待获取锁。

1.3、加锁和解锁的简单流程

假设有两个线程A和B,他们需要争夺基于AQS实现的锁,下面是争夺的简单流程。

  1. 线程A先执行CAS,将state从0修改为1,线程A就获取到了锁资源,去执行业务代码即可。
  2. 线程B再执行CAS,发现state已经是1了,无法获取到锁资源。
  3. 线程B需要去排队,将自己封装为Node对象。
  4. 需要将当前B线程的Node放到双向队列保存,排队。

2、ReentrantLock

2.1、加锁源码分析

ReentrantLock分为公平锁和非公平锁。在加锁的时候因这两种锁的不同会有不同的加锁方式。

ReentrantLock默认是非公平锁,构造方法中传入false则是公平锁。

非公平锁的lock()方法会直接基于CAS尝试获取锁,如果成功的话则执行setExclusiveOwnerThread()方法表示当前线程持有该锁;如果失败则执行acquire()方法。

公平锁则是直接执行acquire()方法。下面是源码对比。
请添加图片描述
接下来的重点则是看acquire()的具体操作。

tryAcquire()方法会再次尝试获取锁,如果成功返回true,否则返回false

可以看到如果失败的话则将请求放到等待队列中同时发送中断信号。
在这里插入图片描述

2.1.1、tryAcquire()的具体实现

  • 非公平锁
    非公平锁会尝试再次直接通过CAS获取锁资源。因为是可重入锁,所以当锁的持有者是当前线程时也可直接获取锁,然后计数器加一。
    请添加图片描述

  • 公平锁
    公平锁的逻辑与非公平锁类似,只不过再获取锁之前会先判断AQS中自己是不是排在第一位,之后才会获取锁。
    请添加图片描述

2.1.2、acquirQueued()的具体实现

在这里插入图片描述
tryAcquire()返回false,即获取锁失败,就开始尝试将当前线程封装成Node节点插入到AQS的结尾。

在插入时我们会看到if(p == head && tryAcquire(arg))这样的语句。

这是因为AQS有伪头结点,所以当这个线程插入到AQS中时发现自己的上一个节点是头结点,即自己排在第一位,那无论是公平锁还是非公平锁自己都可以再次测试获取锁。所以会再次执行tryAcquire()

final boolean acquireQueued(final Node node, int arg) {
    // 不考虑中断
    // failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 拿到当前节点的前继节点
            final Node p = node.predecessor();
            // 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。
            if (p == head && tryAcquire(arg)) {
                // 获取锁资源成功
                setHead(node);
                p.next = null; 
                // 获取锁失败标识为false
                failed = false;
                return interrupted;
            }
            // 没拿到锁资源……
            // shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,
            // 如果不能,就返回false,继续下次循环
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 这里基于Unsafe类的park方法,将当前线程挂起
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            // 在lock方法中,基本不会执行。
            cancelAcquire(node);
    }
}

2.1.3、tryLock的具体实现

无参的tryLock()比较简单,和tryAcquire()基本没区别。

这里主要讲解有参的tryAcquireNanos(int arg, long nanosTimeout)

它的作用在一个时间内尝试获得锁。在这个时间内没有获得锁会挂起park线程。如果成功则返回true,时间结束还没有获得则返回false

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

该方法需要处理中断异常,和lock()方法不一样。

我们继续深入。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
}

可以看到,它直接通过线程的中断标志位决定是否抛出异常。

之后进行tryAcquire(),这个方法细节上面分析过,它有公平和非公平两种实现,简而言之就是非公平直接尝试CAS加锁,公平则是进入队列排队。

也就是说,最后它会正常加锁,只有失败时才会执行doAcquireNanos()。所以有参的tryLock()方法park线程的细节就在其中。

那下面就看看这个方法的内部。

核心就是线程会被封装Node放到队列中,之后查看时间,如果时间比较长,就park线程直到时间结束后再尝试获取锁;如果时间比较短,就在死循环中等到时间结束然后再次获得锁。

因为park的线程主要会因两个动作结束park,即时间到,或者线程发出中断状态,所以最后会查看park是因为什么结束的。如果是中断则抛出异常,否则尝试获取锁。

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 如果等待时间是0秒,直接告辞,拿锁失败  
    if (nanosTimeout <= 0L)
        return false;
    // 设置结束时间。
    final long deadline = System.nanoTime() + nanosTimeout;
    // 先扔到AQS队列
    final Node node = addWaiter(Node.EXCLUSIVE);
    // 拿锁失败,默认true
    boolean failed = true;
    try {
        for (;;) {
            // 如果在AQS中,当前node是head的next,直接抢锁
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 结算剩余的可用时间
            nanosTimeout = deadline - System.nanoTime();
            // 判断是否是否用尽的位置
            if (nanosTimeout <= 0L)
                return false;
            // shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 避免剩余时间太少,如果剩余时间少就不用挂起线程
                nanosTimeout > spinForTimeoutThreshold)
                // 如果剩余时间足够,将线程挂起剩余时间
                LockSupport.parkNanos(this, nanosTimeout);
            // 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。
            if (Thread.interrupted())
                // 是中断唤醒的!
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

2.1.5、总结

ReentrantLock的加锁有公平锁和非公平锁两种方式。

对于非公平锁,任务一开始会直接尝试通过CAS获取锁,失败后才会进入任务队列。并且进入的时候会再次尝试获取锁。整个过程并不考虑其他节点等了多久,所以才是非公平锁。

对于公平锁,任务会按序先进入任务队列,直到有人唤醒他们才会开始获取锁。



网站公告

今日签到

点亮在社区的每一天
去签到