欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 维修 > Java学习笔记(多线程):ReentrantLock 源码分析

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

2025/4/18 7:10:26 来源:https://blog.csdn.net/sinat_38393872/article/details/146443414  浏览:    关键词:Java学习笔记(多线程):ReentrantLock 源码分析

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


  • 1、AQS 概述
    • 1.1、锁的原理
    • 1.2、任务队列
      • 1.2.1、结点的状态变化
    • 1.3、加锁和解锁的简单流程
  • 2、ReentrantLock
    • 2.1、加锁源码分析
      • 2.1.1、tryAcquire()的具体实现
      • 2.1.2、acquirQueued()的具体实现
      • 2.1.3、tryLock的具体实现
      • 2.1.5、总结

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; // 获取锁失败标识为falsefailed = 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);// 拿锁失败,默认trueboolean failed = true;try {for (;;) {// 如果在AQS中,当前node是head的next,直接抢锁final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = 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获取锁,失败后才会进入任务队列。并且进入的时候会再次尝试获取锁。整个过程并不考虑其他节点等了多久,所以才是非公平锁。

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


版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词