• ReentrantLock(公平锁、非公平锁)可重入锁原理


    基本使用

    ReentrantLock,位于java.util.concurrent包,于JDK1.5引入,一种可重入互斥Lock ,其基本行为和语义与使用synchronized方法和语句访问的隐式监视器锁相同,但具有扩展功能。
    ReentrantLock的使用也很简单,在源码注释中可以看到使用的推荐方式:

    1. public void m() {
    2. lock.lock(); // block until condition holds
    3. try {
    4. // ... method body
    5. } finally {
    6. lock.unlock()
    7. }
    8. }

    具体使用代码如下:

    1. package com.starsray.test.lock;
    2. import java.util.concurrent.CountDownLatch;
    3. import java.util.concurrent.locks.ReentrantLock;
    4. public class TestReentrantLock {
    5. private final static ReentrantLock lock = new ReentrantLock();
    6. static int threadCount = 5;
    7. static int counter = 0;
    8. static int threshold = 1000;
    9. private static final CountDownLatch latch = new CountDownLatch(threadCount);
    10. public static void m() {
    11. lock.lock();
    12. try {
    13. counter++;
    14. } finally {
    15. lock.unlock();
    16. }
    17. }
    18. public static void main(String[] args) throws InterruptedException {
    19. for (int i = 0; i < threadCount; i++) {
    20. new Thread(() -> {
    21. for (int j = 0; j < threshold; j++) {
    22. m();
    23. }
    24. latch.countDown();
    25. }, "Thread-" + i).start();
    26. }
    27. latch.await();
    28. System.out.println("count:" + counter);
    29. }
    30. }

    源码分析

    ReentrantLock由上次成功锁定但尚未解锁的线程拥有。当锁不被另一个线程拥有时,调用lock的线程将返回,成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。可以使用方法isHeldByCurrentThreadgetHoldCount来检查。
    ReentrantLock除了支持synchronized的语义之外,还支持公平锁和非公平锁的拓展,在非公平锁模式下与synchronized相同,也是其默认的锁模式。构造函数接受一个可选的公平参数,当设置为true时表示为公平锁。
    ReentrantLock基于AQS实现,内部维护了一个CLH队列,所谓公平非公平,指的是线程是否按照入队的顺序进行锁的获取。需要注意的是,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。另请注意,不定时的tryLock()方法不遵守公平设置。如果锁可用,即使其他线程正在等待,它也会成功。
    查看ReentrantLock的类图结构,其实现了Lock接口,其中定义了关于锁的一些基本操作,包含了SyncFairSyncNonfairSync这几个内部类。从名称也可以看出FairSyncNonfairSync分别实现了公平锁和非公平锁,二者同时继承自,作为锁实现的Sync,并且Sync继承自AbstractQueuedSynchronizerReentrantLock.png
    接下来对ReentrantLock的源码进行分析。

    Sync(基础类)

    ReentrantLock实现的基础依赖于Sync类,一个静态内部抽象类,继承自AbstractQueuedSynchronizer,使用AQS中的state状态值来记录持有锁的次数,为下面的公平锁和非公平锁扩展定义好了基础框架。
    ReentrantLock中关于加锁根据场景有不同的实现,对应的lock方法分别在Lock接口和内部类SyncFairSyncNonfairSync中进行定义和重载,而解锁的方式统一调用的是AQS中定义的变更状态方法,得益于AQS优秀的设计。如下代码所示:

    1. public void unlock() {
    2. // 此处release调用为synch子类对象指向Sync继承自AQS的父类方法
    3. sync.release(1);
    4. }
    5. public void lock() {
    6. // 此处lock方法为sync子类对象各自引用的调用
    7. sync.lock();
    8. }

    AQS中实现了同步队列阻塞以及释放锁的能力,开放了子类关于锁的添加和释放的方法,tryAcquireSharedtryAcquiretryReleasetryReleaseShared,因此ReentrantLock中也是如此,由于其为互斥锁实现,因此只用重写tryAcquiretryRelease方法的实现即可。这在Sync类中分别对应:

    1. // 默认实现了非公平锁的加锁条件
    2. final boolean nonfairTryAcquire(int acquires) {
    3. // 获取当前线程
    4. final Thread current = Thread.currentThread();
    5. int c = getState();
    6. if (c == 0) {
    7. // 如果同步状态位为0,当前线程尝试通过CAS去修改状态位
    8. if (compareAndSetState(0, acquires)) {
    9. // 如果CAS成功,设置当前线程为独占线程,返回true
    10. setExclusiveOwnerThread(current);
    11. return true;
    12. }
    13. }
    14. // 如果不为0,判断当前线程是否为独占线程
    15. else if (current == getExclusiveOwnerThread()) {
    16. // 如果再次获取锁,记录重入次数
    17. int nextc = c + acquires;
    18. if (nextc < 0) // overflow 溢出
    19. throw new Error("Maximum lock count exceeded");
    20. // 更新状态位
    21. setState(nextc);
    22. return true;
    23. }
    24. // 如果线程首次即没有获得锁,也不满足重入条件,返回false,获取锁失败
    25. return false;
    26. }
    27. // 使用模板方法设计模式,该方法在AQS的release方法中被调用,作为判断是否释放锁状态的前置条件
    28. protected final boolean tryRelease(int releases) {
    29. // 每次释放锁对状态为进行递减
    30. int c = getState() - releases;
    31. // 如果当前线程不是队列首线程,抛出IllegalMonitorStateException异常
    32. if (Thread.currentThread() != getExclusiveOwnerThread())
    33. throw new IllegalMonitorStateException();
    34. // 当同步状态位state==0时,返回释放锁成功
    35. boolean free = false;
    36. if (c == 0) {
    37. free = true;
    38. // 当前独占线程为null,即没有线程占用
    39. setExclusiveOwnerThread(null);
    40. }
    41. // 更新同步状态位为初始值
    42. setState(c);
    43. return free;
    44. }

    Sync中tryRelease对应为父类AQS中的实现,nonfairTryAcquire对应子类中的公共实现,父类AQS中tryAcquire的实现分别在FairSyncNonfairSync子类中进行实现。

    FairSync(公平锁)

    由于ReentrantLock基于FIFO的队列来实现线程排队等待,公平锁中的tryAcquire方法,当CPU进行调度时,每次按照队列的排队顺序进行同步状态为值的判断,尝试锁的获取,因此实现了先到先得的公平性。

    1. static final class FairSync extends Sync {
    2. private static final long serialVersionUID = -3000897897090466540L;
    3. final void lock() {
    4. // 调用AQS的acquire方法,传入参数state值为1
    5. // 是否将当前线程加入到等待队列中,取决于tryAcquire的返回结果
    6. // 如果尝试获取锁或者添加到队列失败,将中断当前线程操作
    7. acquire(1);
    8. }
    9. // 尝试获取锁的公平锁实现,如果不是队列首线程将获取锁失败
    10. protected final boolean tryAcquire(int acquires) {
    11. // 获取当前运行线程
    12. final Thread current = Thread.currentThread();
    13. // 获取AQS的同步状态值
    14. int c = getState();
    15. if (c == 0) {
    16. // hasQueuedPredecessors方法的执行结果:如果当前线程之前有排队线程,则为true ,如果当前线程位于队列的头部或队列为空,则为false
    17. // 如果同步状态为0,并且当前队列没有线程排队,并且CAS修改状态位成功则尝试获取锁成功
    18. if (!hasQueuedPredecessors() &&
    19. compareAndSetState(0, acquires)) {
    20. // 设置当前线程为独占拥有线程
    21. setExclusiveOwnerThread(current);
    22. return true;
    23. }
    24. }
    25. else if (current == getExclusiveOwnerThread()) {
    26. // 如果 c != 0,且当前线程为独占线程,记录当前线程重入锁的次数。
    27. int nextc = c + acquires;
    28. if (nextc < 0)
    29. // 冲入次数溢出了,超过了int表示的最大值
    30. throw new Error("Maximum lock count exceeded");
    31. // 更新AQS同步状态位的值
    32. setState(nextc);
    33. return true;
    34. }
    35. return false;
    36. }
    37. }

    NonfairSync(非公平锁)

    关于非公平锁的实现也是相当巧妙,基于队列实现公平锁很简单,如何实现非公平锁呢?如下代码所示:

    1. static final class NonfairSync extends Sync {
    2. private static final long serialVersionUID = 7316153563782823691L;
    3. final void lock() {
    4. // 在线程入队前先进性一次CAS尝试,如果成功,则将当前线程设置为独占模式的执行线程
    5. // 其余线程继续保持队列排队等待状态,作者也在注释中说明了锁的公平性并不能保证线程调度的公平性
    6. if (compareAndSetState(0, 1))
    7. setExclusiveOwnerThread(Thread.currentThread());
    8. else
    9. // 如果CAS失败,则进入队列
    10. acquire(1);
    11. }
    12. protected final boolean tryAcquire(int acquires) {
    13. // 调用基础类中的实现
    14. return nonfairTryAcquire(acquires);
    15. }
    16. }

    总结

    • ReentrantLock实现了synchronized相同的语义,并且对其进行了扩展,synchronized借助于操作系统层面互斥信号量来实现互斥锁,ReentrantLock借助于AQS抽象同步队列框架来实现锁。
    • 公平锁与非公平锁,二者的区别在于,公平锁直接依赖队列的顺序进行线程的执行与挂起,而非公平锁在CPU进行调度时直接进行一次CAS操作,如果成功则获得调度优先级,失败则进入队列等待。
    • 在锁的获取方面使用synchronized的锁往往会被一个线程多次获取,而ReentrantLock提供的锁在线程执释放锁后,如果再次获得锁就需要进入队列等待。

    参考资料:

    • jdk1.8.0_311 ReentrantLock源码
  • 相关阅读:
    深度学习基础知识 Batch Normalization的用法解析
    leetcode_17电话号码的组合
    Pytorch 的基本概念和使用场景介绍
    UnityPlayerActivity详解
    大数据时代,数据治理
    【kafka】——概述&安装
    spring-boot-plus2.7.12版本重磅发布,三年磨一剑,兄弟们等久了,感谢你们的陪伴
    28、JavaScript学习笔记——脚本化CSS
    leetcode48. 旋转图像
    仿牛客项目总结
  • 原文地址:https://blog.csdn.net/qq_38721537/article/details/125902920