【并发专题】深入理解AQS独占锁之ReentrantLock源码分析

不管现实多么惨不忍睹,都要持之以恒地相信,这只是黎明前短暂的黑暗而已。不要惶恐眼前的难关迈不过去,不要担心此刻的付出没有回报,别再花时间等待天降好运。真诚做人,努力做事!你想要的,岁月都会给你。【并发专题】深入理解AQS独占锁之ReentrantLock源码分析,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

前置知识

Q1:你能描述在多线程下竞争排他锁的大概业务过程吗?
答:在多个线程在竞争锁对象的时候,只有一条线程会竞争成功。其余失败的线程将阻塞等待。直到持有锁的线程执行完任务释放锁之后,唤醒在阻塞等待的其他线程,然后其他线程继续竞争锁。由此循环往复,直到所有竞争线程执行完。
上面涉及的因素有:互斥、锁(共享资源)、阻塞队列(保存因竞争失败的线程)、等待通知机制(原持有锁线程释放后唤醒阻塞等待线程)

Q2:什么是线程间的通信?
答:线程间通信,可以简单的理解为当多个线程共同操作共享的资源时,提供一种机制,让线程互相告知自己的状态,以协调线程,避免资源争夺。
上面涉及的因素有:线程间通信(同步、协调)

课程内容

一、管程——Java同步的设计思想

在【前置知识】的提问中,我们重温了一遍锁实现的过程,以及什么是线程间的通信。那么,如果让我们设计一把锁,你觉得需要考虑什么问题呢?我们站在巨人的肩膀上可以分析出来,我们需要考虑的问题有:共享变量的设计、互斥共享、线程间通信等。所以,就有这么一种设计思想,叫管程。

管程是什么

管程是一种设计,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译成JAVA语言,其实就是管理类的成员变量和方法,让线程安全;

在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。它的设计模型如下:
在这里插入图片描述

MESA是如何解决互斥问题

从上图我们可以看出,管程将共享变量以及对其的操作统一封装了起来,并且通过一个入口等待队列,将并发访问共享对象的操作,一定程度上变成了队列出栈入栈的并发操作。在此期间,保证出栈入栈的互斥性,只允许一个线程进入管程。

MESA是如何解决协调问题

什么是同步问题?其实就是线程之间的协作,例如线程T2依赖于线程T1,只有线程T1执行完,T2才能执行。
从上图我们可以看出来,管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用就是为了解决线程之间的同步问题的(这样说比较专业,大白话说就是,让多个线程在条件不满足的时候等待,条件满足的时候,如何让这些阻塞等待的线程有序执行,以避免资源争夺)。

Java如何实现管程

Java中针对管程有两种实现,分别如下:

  1. 一种是基于Object的Monitor机制,用于synchronized内置锁的实现
  2. 一种是抽象队列同步器AQS,用于JUC包下Lock锁机制的实现

二、AQS原理分析

2.1 什么是AQS

java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的,他们的实现方式也很相似:一般是通过一个内部类Sync继承 AQS(AbstractQueuedSynchronizer),然后将同步器所有调用都映射到Sync对应的方法。比如,给大家看一份ReentrantLock的类结构:
在这里插入图片描述
在这里插入图片描述
下图,是 AQS(AbstractQueuedSynchronizer)类的子类:
在这里插入图片描述

2.2 AQS的特性

AQS具备的特性:

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

2.3 AQS核心结构

AQS内部维护属性volatile int state,表示可用的资源数目。也是我们上面说的MESA模型中的【共享变量】。它的访问方式有三种:

getState() :获取当前state值
setState() :在【可重入】的时候,设置state值
compareAndSetState():用来多线程CAS竞争的时候,设置state。谁先设置成功,则谁获得锁

AQS定义了两种资源访问方式:

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock
  • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

AQS实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

2.4 AQS定义两种队列

  • 同步等待队列: 主要用于维护获取锁失败时入队的线程。
  • 条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁(PS:无论是Object的wait/notify方法,还是现在说的await/signal方法,本质上调用的是LockSupport对应的park/unpark方法对应的native方法,即调用系统函数)。

2.5 AQS定义5个队列状态

AQS 定义了5个队列中节点状态:

  1. 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
  2. CANCELLED,值为1,表示当前的线程被取消;
  3. SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
  4. CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
  5. PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

注意上面的 第3点,SIGNAL状态,阻塞等待队列中的线程就是这个状态值,在后面的ReentrantLock源码分析中,我们就见到。

*2.6 同步等待队列和条件队列

AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
AQS 依赖CLH同步队列来完成同步状态的管理:(重点

  • 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
  • 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
  • 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)

下图是【条件等待队列】中的头节点线程,通过【尾插法】的方式加入到【同步等待队列】
在这里插入图片描述
AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:

  • 调用await方法阻塞线程;
  • 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)

三、ReentrantLock源码分析

当我们调用ReentrantLock的lock方法的时候,在非公平锁的实现方式下,它的关键源码流程图如下图所示,下面我们也会配合该图讲解一下关键源码:

lock()非公平锁关键源码流程图

在这里插入图片描述
(PS:根据流程图,按照时间顺序来解读关键源码)
首我们先梳理一下上图中涉及到的三个核心类,分别是:ReentrantLock,ReentrantLock.Sync(NonfairSync),AbstractQueuedSynchronizer(AQS)。流程图所有关键方法,都包含在这三个类里面:

ReentrantLock#lock()

  • 方法介绍:获取锁。在这里,实际上是调用内部类Sync的子类,NonfairSync或者FairSync的lock()方法
  • 源码图如下:
    在这里插入图片描述

AbstractQueuedSynchronizer#compareAndSetState(AQS)

  • 方法介绍:使用CAS方式设置state状态。在这里并没有直接对【共享变量:state】字段做操作,而是通过【当前对象地址+偏移量】的方式索引state字段,再调用本地native方法CAS设置【共享变量:state】字段值为1。(PS:这里是第1次尝试获取锁)
  • 源码图如下:
    在这里插入图片描述

ReentrantLock.Sync(NonfairSync)#setExclusiveOwnerThread

  • 方法介绍:当上面的CAS设置操作成功,则代表线程获取到了锁,所以,需要设置当前锁的独占线程exclusiveOwnerThread字段。这个字段也是后面实现【可重入】的关键,即:判断申请线程是不是当前持有锁线程
  • 源码图如下:
    在这里插入图片描述

AbstractQueuedSynchronizer#acquire()(AQS)

  • 方法介绍:这个方法本质上是调用了后面要提到的两个tryAcquire()acquireQueued()方法。也是一个用来获取锁的方法
  • 源码图如下:
    在这里插入图片描述

ReentrantLock.Sync(NonfairSync)#nonfairTryAcquire

  • 方法介绍:tryAcquire本质上会调用的sync的具体实现方法。这里演示的是非公平锁的实现方法。方法用来再次尝试获取锁。
  • 源码图如下:
    在这里插入图片描述
  • 方法解读:
    • 首先会先判断当前锁状态,如果没有被持有c == 0,那么:
      • 再次尝试调用最上面提到的compareAndSetState方法以获取锁,如果能获取成功,设置当前占有线程==(PS:这里是第2次尝试获取锁)
    • 如果c != 0,那么判断一下当前线程是否为持有锁线程,如果是,则走【可重入】那套逻辑。再看看,其实可重入没啥奥妙,就是将【共享变量:state】值再加1而已,计数获取了多少次。

AbstractQueuedSynchronizer#addWaiter(AQS)

  • 方法介绍:将当前线程封装成Node节点,添加到等待队列当中。
  • 源码图如下:
    在这里插入图片描述
  • 方法解读:这个方法其实就是干了一件事,入队(入队出队操作及步骤,大家参考双端队列是如何出队入队的就好了,建议画图理解,事半功倍)。只不过,初次使用队列需要初始化双端队列的head节点跟tail节点。另外,这里也有一点值得注意的是,这里的入队采用的是【尾插法】,另外,入队操作都是使用CAS来完成的,这就是跟一开始说的【MESA将并发访问共享对象的操作,一定程度上变成了队列出栈入栈的并发操作】呼应了。

AbstractQueuedSynchronizer#acquireQueued(AQS)

  • 方法介绍:获得队列。这个名字取的很简单,却是一个非常核心的方法,AQS同步等待队列的核心逻辑就是这里。开始做阻塞被唤醒之前的准备。但是如果线程node是队头,则还有一次再次尝试获取锁的机会。如果不是,则开始阻塞(PS:这里是第3次尝试获取锁)
  • 源码图如下:
    在这里插入图片描述
  • 方法解读:这里又用了一个【自旋】操作,而且非常巧妙。首先,【自旋】开始的时候,会先判断当前节点的前驱节点是否为head节点,如果是的话,则再次尝试获取锁(PS:从这里我们可以看出,ReentrantLock在设计的时候,会尽可能地在入阻塞等待队列之前让线程尝试获取锁,这是因为,阻塞等待带来的上下文切换代价是昂贵的)。如果不是,则开始执行shouldParkAfterFailedAcquire()方法以及parkAndCheckInterrupt()方法,来让当前线程进入阻塞等待状态。当线程被阻塞唤醒之后,会再次从parkAndCheckInterrupt()方法处开始继续【自旋】,重复当前线程操作

AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire(AQS)

  • 方法介绍:判断是否需要把线程节点在上一次尝试获取失败之后,修改状态为阻塞等待
  • 源码图如下:
    在这里插入图片描述
  • 方法解读:我们回过头去看int waitStatus字段,会发现我们在new Node()的时候并没有给它设初始值,然后又是int类型,所以默认值为0,于是,在第一次的acquireQueued()自旋体内,会走下面的红框圈住的代码。第二次自旋才会从第一个if (ws == Node.SIGNAL)中跳出去

AbstractQueuedSynchronizer#parkAndCheckInterrupt(AQS)

  • 方法介绍:阻塞等待当前线程,并且检查当前线程的中断标志
  • 源码图如下:
    在这里插入图片描述
  • 方法解读:调用了当前方法后,线程将会在这里开始阻塞,并进入等待(WAITTING)状态。当然,当线程被唤醒的时候也是从这里开始运行的

学习总结

  1. 学习了MESA模型,并且了解了一个锁的设计原理
  2. 学习了AQS,AbstractQueueSynchronizer的设计原理。通过定义的共享变量state,以及条件等待队列和同步等待队列,实现了丰富的线程间通信,即资源获取功能
  3. 深入学习了ReentrantLock源码

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/180532.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!