ReentrantLock实现原理-非公平锁

news2024/11/24 9:11:32

在线程间通信方式2一节中,我们了解了Lock,Condition和ReentrantLock,学习了简单使用Condition和RentrantLock完成线程间通信,从文章中我们了解到ReentrantLock是Lock接口的一个最常用的实现类,ReentrantLock是独占锁,独占锁的场景下又支持公平锁和非公平锁,那么在源码实现中,ReentrantLock继承关系,实现结构又是怎样的呢?

ReentrantLock继承关系及关联类

在多线程与锁中,我们了解到ReentrantLock是支持公平锁和非公平锁的,对应的构造函数源码如下:

// ReentrantLock.java
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

可以看到如果是公平锁则创建FairSync对象,如果是非公平锁则创建NonfairSync,再结合Lock接口核心的lock,tryLock,unlock函数源码(Lock接口在线程间通信方式2中有介绍)可以看出,在ReentrantLock中,使用FairSync或NonfairSync代理了锁状态管理,lock,tryLock和unlock实现源码如下:

// ReentrantLock.java
public void lock() {
    sync.lock();
}

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

public void unlock() {
    sync.release(1);
}

由此,进一步梳理ReentrantLock实现,可以得到下图:

reentrantlock_extend.drawio

上图中一些生僻类及其作用见下表:

类名说明备注
FairSyncReentrantLock中公平锁的实现类/
NonfairSyncReentrantLock中非公平锁的实现类/
Sync公平锁和非公平锁实现类的共同父类,使用AbstractQueuedSynchronizer状态记录锁的持有数据/
AbstractQueuedSynchronizerAbstractQueuedSynchronizer简称AQS,一个用于实现阻塞锁和同步器的工具类,其内部维护一个先进先出的等待队列(真实数据结构是双向链表),依赖一个int型的数据管理同步状态/
AbstractOwnableSynchronizerAQS的父类,定义了一个线程独占的同步器,用于实现创建锁和锁占有标记的基础类,其内部使用exclusiveOwnerThread记录当前占用锁的线程/
Serializable序列化接口/

ReentrantLock.lock流程分析

跟踪ReentrantLock.lock调用流程,可以得到下面的时序图(以非公平锁为例分析):

ReentrantLock.lock

上图中描述了ReentrantLock中lock在资源空和资源被占用的情况下的执行流程,接下来我们来看下其内部的细节实现。

ReentrantLock.lock获取锁成功

NonfairSynck类中的lock代码如下所示:

// NonfairSync.java
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

可以看出,我们在调用了ReentrantLock.lock后,NonfairSync会首先尝试通过CAS将资源占用状态置为1(compareAndSetState,默认值0,期望值1),如果执行成功,说明当前获取共享资源成功,将当前线程设置为独占锁持有者,当前线程继续执行。

compareAndSetState

compareAndSetState操作的是AQS中声明的一个int型的值,如果其值为0,表示当前锁空闲,如果有线程到来可以占用锁,如果值大于1,表示当前锁被占用,为保证多线程对该值的操作实时可见,使用volatile修饰该变量,相关代码如下:

// AbstractQueuedSynchronizer.java
private volatile int state;
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
setExclusiveOwnerThread

setExclusiveOwnerThread最终是将当前线程设置到AbstractOwnableSynchronizer中定义的exclusiveOwnerThread中,代码如下:

// AbstractOwnableSynchronizer.java
private transient Thread exclusiveOwnerThread;

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

transient关键字:

transient关键字的主要作用是让某些被transient关键字修饰的变量不被序列化,如果对transient修饰的变量执行了序列化,则该变量会重新执行默认初始化,反序列化得到的对象是null

ReentrantLock.lock获取锁失败

前文中可以看到如果compareAndSetState执行返回false的话,就说明当前共享资源被占用,随后走else逻辑,执行acquire(1),其内容如下:

// NonfairSync.java
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

其内部主要由两块组成,tryAcquire和acquireQueued,其中tryAcquire再次尝试获取锁,如果获取成功,则流程结束,当前线程正常继续执行,如果获取失败,则执行acquireQueued方法(由于使用且操作符连接tryAcquire和acquireQueued,所以只有tryAcquire返回false的时候,才会执行acquireQueued方法),该方法接受addWaiter返回的Node参数,下面来详细看下两个函数的具体实现。

tryAcquire

tryAcquire在NonfairSync中的实现如下,其最终调用到的是Sync类的nonfairTryAcquire方法:

// NonfairSync.java
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
// Sync.java
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取当前同步资源状态
    int c = getState();
    // 同步资源空闲
    if (c == 0) {
        // 占用同步资源更新资源状态
        if (compareAndSetState(0, acquires)) {
            // 设置当前线程为资源对应的独占锁持有者
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程就是持有锁线程,更新锁状态,直接持有锁
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可以看到在tryAcquire中实际上也是尝试获取锁的过程,首先检查当前当前同步资源状态,如果不可获取,则检查当前线程时否是持锁线程,是的话则直接获取锁(可重入锁的实现),更新同步资源状态,如果均失败,则返回false。

acquireQueued

acquireQueued在AQS中关联的核心代码如下所示,可以看出主要包含addWaiter和acquireQueued两部分:

addWaiter
// AbstractQueuedSynchronizer.java
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
// AbstractQueuedSynchronizer.java
private Node addWaiter(Node mode) {
    // 创建新的Node对象
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

可以看到不管是addWaiter还是enq,其最终目标都是基于当前线程创建新的Node对象,将新的Node对象添加在队尾,前文提到AQS中维护了一个先进先出的队列,其数据结构本质是双向链表,这里的head,tail就是链表的具体实现。Node类中包含了指向前一个元素和后一个元素的引用,Node的声明如下:

// Node实体类
static final class Node {
    ...
    
    // 前一个元素
    volatile Node prev;
    // 下一个元素
    volatile Node next;
    // 线程对象
    volatile Thread thread;

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
    
    ...
}

// AbstractQueuedSynchronizer.java中声明Node对象
// 链表头
private transient volatile Node head;
// 链表尾部
private transient volatile Node tail;
acquireQueued

acquireQueued源码如下:

// AbstractQueuedSynchronizer.java
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

可以看到在addWaiter中成功将新的Node添加到队尾后,acquireQueued中当前线程会自旋,尝试获取锁,如果获取失败,则执行shouldParkAfterFailedAcquire和parkAndCheckInterrupt,这两个函数都返回true则执行selfInterrupt方法(代码见acquire函数部分)。

shouldParkAfterFailedAcquire用于判断是否应该对当前线程阻塞,如果是的话则返回true,parkAndCheckInterrupt用于执行线程阻塞并且判断当前线程是否处于interrupt状态,如果是则会返回true。通过源码可以看到其是通过LockSupport.park进行线程状态切换的,代码如下:

// AbstractQueuedSynchronizer.java
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

LockSupport.park作用

LockSupport.park用于在许可证不可用时阻塞禁用当前线程,如果许可证可用,则该许可证被消耗,并且当前调用立即返回,否则,出于线程调度的目的,当前线程将被阻塞禁用,并进入休眠状态,直到发生以下三种情况之一:

  • 其他线程以当前线程为参数调用unpark
  • 其他线程中断当前线程
  • 调用发生异常

结合上文,我们可以得出ReentrantLock.lock执行的一般流程如下所示:

reentrantlock_lock2

ReentrantLock.unlock流程分析

跟踪ReentrantLock.unlock调用流程,可以得到下面的时序图(以非公平锁为例分析):

ReentrantLock.unlock

可以看到对于ReentrantLock.unlock流程而言,其核心实现函数是tryRelease和unparkSuccessor,release函数如下所示:

// ReentrantLock.java
public void unlock() {
    sync.release(1);
}

// AQS.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执行完成返回true后,我们会获取链表首位元素,当首位元素不为空且等待状态(waitStatus)不等于0时,执行unparkSuccessor,在这里我们又遇到了Node类的另一个核心元素waitStatus,其声明如下:

static final class Node {
    ...
    
    // waitStatus的四种可能取值,表示当前节点及其后续节点对应线程的运行状态
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    
    // 声明waitStatus
    volatile int waitStatus;
    
    ...
}

四种取值含义如下所示:

  • SIGNAL:取值为-1,该节点的后续节点处于被阻塞或即将阻塞的状态,因此当前节点的线程在释放锁或取消时必须解除后继节点的阻塞状态,使后续节点的线程得以正常运行
  • CANCELLED:取值为1,当前节点对应的线程由于超时或中断而被取消,Node的waitStatus取该值,进入取消状态后节点状态不再变化
  • CONDITION:取值为-2,该节点当前在等待队列中,节点对应的线程等待Condition,当其他线程对Condition调用了signal方法后,该节点从等待队列中转入链表中,进行同步状态的获取
  • PROPAGATE:取值为-3,在共享锁实现中使用,当前节点线程处于可运行状态

为了简化使用,对于waitStatus取值并没有按照数字递增或递减排列进行取值,如果该节点取值为非负值,则代表不需要将操作同步到其他节点,对于普通Node节点而言,waitStatus字段初始化为0,对于条件节点,该字段初始化为1,在代码中使用CAS对该字段进行修改。

下面我们来分别看下这两个核心函数的实现

tryRelease

tryRelease主要用于对锁状态标记进行清理,函数实现如下所示:

protected final boolean tryRelease(int releases) {
    // 获取AQS的锁状态标记,计算剩余锁状态标记
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 锁状态标记为0,当前没有其他线程持有锁,锁处于空闲状态,设置锁持有者为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 更新锁状态
    setState(c);
    return free;
}

在ReentrantLock独占锁实现的场景下,state锁状态标记取值只有两个,0和1(前文lock过程中也有分析通过CAS改变state状态时,一直是从0到1的修改),进而当当前线程执行tryRelease后,state锁状态标记取值更新为0,表示当前锁处于空闲状态,随后自然要唤醒Node链表中的其他节点去获取锁啦。

unparkSuccessor

unparkSuccessor函数主要用于更新Node节点的waitStatus状态并按需将Node链表中阻塞的线程唤醒执行,实现如下所示:

private void unparkSuccessor(Node node) {
    // 获取头节点的waitStatus状态,将头节点的waitStatus状态设置为0,head waitStatus恢复初始状态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 获取头节点的下一个节点,如果下一个节点为null或者状态为取消状态(waitStatus大于0只有取消状态),则从尾节点开始遍历,查找未被取消的后续节点对象
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 对上一步获取到的节点对应的线程执行unpark唤醒,去抢占锁
    if (s != null)
        LockSupport.unpark(s.thread);
}

从前面可以看出这里传入的node是当前的head(链表头),判断当前头节点的waitStatus,如果不是初始值则重置为初始值,随着查找下一个要被唤醒的节点,查找到后,唤醒节点对应的线程,让线程去尝试抢占锁。

结合上文,我们可以得出ReentrantLock.unlock执行的一般流程如下所示:

reentrantlock_unlock.drawio

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/489703.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WEBPACK和ROLLUP构建前端工具库

1. WEBPACK webpack 提供了构建和打包不同模块化规则的库&#xff0c;只是需要自己去搭建开发底层架构。vue-cli&#xff0c;基于 webpack &#xff0c; vue-cli 脚手架工具可以快速初始化一个 vue 应用&#xff0c;它也可以初始化一个构建库。 2. ROLLUP rollup 是一个专门…

prusa2.6.0 树形支撑(有机支撑)Organic体验测试 3d打印及下载失败解决

目前官网没有这个2.6版本&#xff0c;只有2.5.2下载&#xff0c;是没有树形支撑的。如果试用2.6版本&#xff0c;需要从GitHub下载。地址为&#xff1a; https://github.com/prusa3d/PrusaSlicer/releases/tag/version_2.6.0-alpha6 或者点击链接&#xff1a; Release PrusaS…

aop切面调用失效问题排查

应用里有较多的地方访问外部供应商接口&#xff0c;由于公网网络不稳定或者外部接口不稳定&#xff08;重启&#xff0c;发版&#xff0c;ip切换&#xff09;的原因&#xff0c;经常报502或者504错误。为了解决HTTP调用的500报错&#xff0c;选择使用spring的retryable注解进行…

Leetcode292. Nim 游戏

Every day a leetcode 题目来源&#xff1a;292. Nim 游戏 解法1&#xff1a;数学推理 让我们考虑一些小例子。 显而易见的是&#xff0c;如果石头堆中只有一块、两块、或是三块石头&#xff0c;那么在你的回合&#xff0c;你就可以把全部石子拿走&#xff0c;从而在游戏中…

李沐动手学深度学习 v2 实战Kaggle比赛:预测房价

前言 最近学习一些深度学习知识&#xff0c;观看了李沐老师的《动手学深度学习》的视频 练习一下 实战Kaggle比赛&#xff1a;预测房价 巩固一下 前面学习的知识&#xff0c; 不coding一下总感觉什么也没学 陆陆续续调了一天 记录一下 导包 %matplotlib inline import numpy…

计算机网络第二章(谢希仁第八版)

作者&#xff1a;爱塔居 专栏&#xff1a;计算机网络 作者简介&#xff1a;大三学生&#xff0c;希望和大家一起进步 文章目录 目录 文章目录 前言 一、物理层的基本概念 1.1 物理层协议的主要任务 1.2 传输媒体&#xff08;了解&#xff09; 二、传输方式 2.1 串行传输…

第二十八章 Unity射线检测

本章节我们介绍一下射线。射线就是从一个固定点向一个方向发射出一条直线&#xff0c;在发射过程中需要判断该射线有没有与游戏物体发送碰撞。射线既可以用来检测射击游戏中武器指向目标&#xff1b;又可以判断鼠标是否指向游戏物体。射线的创建方式&#xff0c;一般使用代码来…

11. Kubernetes 开章

本章讲解知识点 Kubernetes 概念为什么要使用 KubernetesKubernetes 的部署架构Kubernetes 基本命令本章主要是针对 Kubernetes 基本概念为读者讲解,读者能有一个大概印象即可,不需要过于斟酌细节,针对 Kubernetes 的概念将在后面章节中详细讲解。 1. Kubernetes 概念 我们…

学习Transformer前言(Self Attention Multi head self attention)

一、前言 一直在做项目&#xff0c;也比较懒没有挤出时间去学习新的东西&#xff0c;感觉停滞很久了&#xff0c;好长一段时间都没有新的知识输入&#xff0c;早就需要就去学习transformer了&#xff0c;因此先来学习注意力机制&#xff0c;本文为个人的一个笔记总结。主要是基…

Linux系统编程(三)—— 文件编程(3)进程环境

一、main函数 现在的格式&#xff1a;int main(int argc, char *argv[])以前的main函数有三个参数&#xff0c;另一个参数就是环境变量 二、进程的终止&#xff08;两种都要背下来&#xff09; 2.1 正常终止 &#xff08;1&#xff09;从main函数返回 main函数被称为程序的…

第10章:堆

堆是什么&#xff1f; 堆是一种特殊的完全二叉树。 完全二叉树&#xff1a;每层节点都完全填满&#xff0c;最后一层若是没填满&#xff0c;则只缺少右边的节点。所有的节点都大于等于&#xff08;最大堆&#xff09;或小于等于&#xff08;最小堆&#xff09;它的子节点。jav…

软考——数据结构,算法基础,程序设计语言,法律法规,多媒体基础

数据结构与算法基础 数组与矩阵线性表广义表树与二叉树图排序与查找算法基础及常见算法 数组 稀疏矩阵 直接把&#xff08;0&#xff0c;0&#xff09;带入&#xff0c;排除B&#xff0c;C 将&#xff08;1&#xff0c;1&#xff09;带入&#xff0c;排除D&#xff0c; 最终…

Python | 人脸识别系统 — 博客索引

本博客为人脸识别系统的博客索引 工具安装、环境配置&#xff1a;Python | 人脸识别系统 — 简介 1、UI代码 UI界面设计&#xff1a;Python | 人脸识别系统 — UI界面设计UI事件处理&#xff1a;Python | 人脸识别系统 — UI事件处理 2、用户端代码 用户端博客索引&#xff1a;…

Jupyter Notebook入门教程

Jupyter Notebook&#xff08;又称Python Notebook&#xff09;是一个交互式的笔记本&#xff0c;支持运行超过40种编程语言。本文中我们将介绍Jupyter Notebook的主要特点&#xff0c;了解为什么它能成为人们创造优美的可交互式文档和教育资源的一个强大工具。 首先&#xff…

vue diff算法与虚拟dom知识整理(4) h函数虚拟节点嵌套

那么 先补充上文有一个没强调的点 h函数 当你不需要属性时 其实是可以不传的 例如 我们打开案例 打开 src下的index.js 修改代码如下 import {init,classModule,propsModule,styleModule,eventListenersModule,h,} from "snabbdom";//创建patch函数const patch ini…

CUBLAS 和 CUDNN

文章目录 一、什么是CUBLASCUBLAS实现矩阵乘法CUBLAS中的Leading DimensionCUBLAS LEVEL3函数 &#xff1a; 矩阵矩阵CUBLAS实现矩阵乘法 二、cuDNN使用CuDNN实现卷积神经网络 四、CUBLAS和CUDNN实践 一、什么是CUBLAS cuBLAS是BLAS的一个实现。BLAS是一个经典的线性代数库&am…

解决C语言的缺陷【C++】

文章目录 命名空间展开了命名空间域指定访问命名空间域域作用限定符命名空间定义 C输入&输出缺省参数全缺省参数半缺省参数缺省参数应用 函数重载参数类型不同参数个数不同参数类型顺序不同 引用引用的特性引用在定义时必须初始化一个变量可以有多个引用引用一旦引用一个实…

基于Radon-分数傅里叶变换对消器的海杂波弱目标检测

海面微弱目标检测面临的主要困难来自&#xff1a; 慢速小目标回波微弱&#xff1b;空时变海杂波异常复杂&#xff0c;海杂波特性认知难度大&#xff1b;目标模型难以建立&#xff1b;目标、海杂波类别非平衡。 ARU效应 是由于海面波浪的起伏和涟漪引起的。在雷达回波信号中&am…

nodejs开发 | 安全工具端口扫描器

今天分享一个nodejs的demo&#xff0c;可以扫描出指定IP的端口开放情况。 简单的说 Node.js 就是运行在服务端的 JavaScript。 Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。 Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境&#xff0c;基于 Google 的 V8…

[笔记]Python计算机视觉编程《一》 基本的图像操作和处理

文章目录 前言环境搭建 计算机视觉简介Python和NumPy第一章 基本的图像操作和处理1.1 PIL&#xff1a;Python图像处理类库1.1.1 转换图像格式1.1.2 创建缩略图1.1.3 复制和粘贴图像区域1.1.4 调整尺寸和旋转 1.2 Matplotlib1.2.1 绘制图像、点和线 前言 今天&#xff0c;图…