共享模型之管程(悲观锁)

news2025/3/9 23:44:14

共享模型之管程(悲观锁)


文章目录

  • 共享模型之管程(悲观锁)
  • 一、常见线程安全的类
  • 二、对象头
  • 三、Monitor(监视器 / 管程)
  • 四、偏向锁
    • 偏向锁的实现原理
    • 撤销偏向锁
  • 五、轻量级锁
    • 轻量级锁的释放
  • 六、重量级锁
  • 七、锁的升级流程
  • 八、sleep / wait / park
    • sleep
    • wait
    • park
  • 九、多把锁相关
  • 十、ReentrantLock


一、常见线程安全的类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

他们的每个方法是原子的,但多个方法的组合不是原子的


二、对象头

  • 普通对象头
    在这里插入图片描述
    Mark Word 用来存储对象的 hashCode 或者锁信息等。
    Klass Word 存储到对象类型数据的指针

  • 数组对象头
    在这里插入图片描述
    Array length 存储了数组的长度

  • 其中32位 Mark Word 的结构为
    在这里插入图片描述

  • 其中64位 Mark Word 的结构为
    在这里插入图片描述
    从上到下对应的是无锁、偏向锁、轻量级锁、重量级锁以及GC标志。

可以看到,当对象状态为偏向锁时,Mark Word 存储的是偏向的线程 ID;
当状态为轻量级锁时,Mark Word 存储的是指向线程栈中 Lock Record 的指针;
当状态为重量级锁时,Mark Word 为指向堆中的 monitor(监视器)对象的指针。


三、Monitor(监视器 / 管程)

在 Java 中,监视器(monitor)是一种同步工具,用于保护共享数据,避免多线程并发访问导致数据不一致。在 Java 中,每个对象都有一个内置的监视器。

监视器包括两个重要部分,一个是锁,一个是等待/通知机制,后者是通过 Object 类中的wait(), notify(), notifyAll()等方法实现的。
在这里插入图片描述
刚开始 Monitor 中 Owner 为 null,当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner。

在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED,Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的。

Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态,BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片,BLOCKED 线程会在 Owner 线程释放锁时唤醒,WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争。


四、偏向锁

Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源无竞争情况下消除了同步语句,连 CAS(后面会细讲,戳链接直达) 操作都不做了,着极大地提高了程序的运行性能。

大白话就是对锁设置个变量,如果发现为 true,代表资源无竞争,则无需再走各种加锁/解锁流程。如果为 false,代表存在其他线程竞争资源,那么就会走后面的流程。

偏向锁的实现原理

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费 CAS 操作来加锁和解锁(对比来说轻量级锁每次都需要生成锁记录,然后用锁记录替换 markword );如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用 CAS 来替换 Mark Word 里面的线程 ID 为新线程的 ID,这个时候要分两种情况:

成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;

失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

CAS 是比较并设置的意思,用于在硬件层面上提供原子性操作。在 在某些处理器架构(如x86)中,比较并交换通过指令 CMPXCHG 实现((Compare and Exchange),一种原子指令),通过比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
在这里插入图片描述
图中涉及到了 lock record 指针指向当前堆栈中的最近一个 lock record,是轻量级锁按照先来先服务的模式进行了轻量级锁的加锁。

撤销偏向锁

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:

在一个安全点(在这个时间点上没有字节码正在执行)停止拥有锁的线程。
遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Mark Word,使其变成无锁状态。
唤醒被停止的线程,将当前锁升级成轻量级锁。
所以,如果应用程序里所有的锁通常处于竞争状态,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭。

调用对象的 hashCode 函数时会生成 hashCode,但对于偏向锁 Mark Word 里记录的是线程 id,没地方放哈希值,所以会撤销偏向锁。轻量级锁在锁记录里记录 hashCode,重量级锁会在 Monitor 中记录 hashCode。

调用 wait() 或 notify() 方法会触发偏向锁的撤销,并升级为重量级锁。这是因为 wait() 和 notify() 引入了线程间的竞争和同步机制,而偏向锁无法应对这种场景。

在这里插入图片描述


五、轻量级锁

多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。

JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。

然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

自旋:不断尝试去获取锁,一般用循环来实现。

自旋是需要消耗 CPU 的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费 CPU 资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环 10 次,如果还没获取到锁就进入阻塞状态。

但是 JDK 采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

自旋也不是一直进行下去的,如果自旋到一定程度(和 JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。

轻量级锁的释放

在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程。

在这里插入图片描述


六、重量级锁

重量级锁依赖于操作系统的互斥锁(mutex,用于保证任何给定时间内,只有一个线程可以执行某一段特定的代码段) 实现,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。

每一个对象都可以当做一个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:

Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry List
Wait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
Owner:获得锁的线程称为 Owner
!Owner:释放锁的线程

当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到 Contention List 队列的队首,然后调用park 方法挂起当前线程。

当线程释放锁时,会从 Contention List 或 EntryList 中挑选一个线程唤醒,被选中的线程叫做Heir presumptive即假定继承人,假定继承人被唤醒后会尝试获得锁,但synchronized是非公平的,所以假定继承人不一定能获得锁。

这是因为对于重量级锁,如果线程尝试获取锁失败,它会直接进入阻塞状态,等待操作系统的调度。

如果线程获得锁后调用Object.wait方法,则会将线程加入到 WaitSet 中,当被Object.notify唤醒后,会将线程从 WaitSet 移动到 Contention List 或 EntryList 中去。需要注意的是,当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。


七、锁的升级流程

每一个线程在准备获取共享资源时: 第一步,检查 MarkWord 里面是不是放的自己的 ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。

第二步,如果 MarkWord 不是自己的 ThreadId,锁升级,这时候,用 CAS 来执行切换,新的线程根据 MarkWord 里面现有的 ThreadId,通知之前线程暂停,之前线程将 Markword 的内容置为空。

第三步,两个线程都把锁对象的 HashCode 复制到自己新建的用于存储锁的记录空间,接着开始通过 CAS 操作, 把锁对象的 Markword 的内容修改为自己新建的记录空间的地址的方式竞争 MarkWord。

第四步,第三步中成功执行 CAS 的获得资源,失败的则进入自旋 。

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。

第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。


八、sleep / wait / park

sleep

sleep 是 Thread 类的静态方法,它的作用是让当前线程暂停执行一段指定的时间(毫秒或纳秒),到时间后自动恢复,无需外部干预,但暂停期间不会释放持有的锁

wait

wait 是Object类的实例方法,它的作用是让当前线程暂停执行,进入waitSet等待,并释放持有的锁,直到其他线程调用 notify() 或 notifyAll() 唤醒它。
与sleep不同的是它会释放锁,并且需要 synchronized 配合,需要外部唤醒。

park

park 是 LockSupport 类的静态方法,它的作用是暂停线程的执行,直到其他线程调用 unpark() 或线程被中断,暂停期间不会释放持有的锁
它不需要配合 synchronized,也不会释放锁资源,unpark() 会提前提供一个许可证,下次 park 时不会进入阻塞。


九、多把锁相关

多把锁的优势是可以增加并发度,但是如果一个线程需要多把锁就容易发生死锁,例如哲学家就餐问题。死锁属于活跃性问题,除了死锁还有活锁和饥饿两种情况。活锁是两个线程互为对方的结束条件而无法结束,饥饿则是一个线程的优先级太低,始终无法得到CPU的调度,拿不到锁。


十、ReentrantLock

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

  1. 可重入锁的等待期间可以被 interrupt 打断。
  2. 获取锁超时会立即失败。
  3. ReentrantLock 默认是不公平的,也支持公平模式。
  4. ReentrantLock 支持多个条件变量(多间休息室)。
  5. 内部维护持有计数,记录锁被同一线程获取的次数,确保完全释放。

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

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

相关文章

零基础C语言学习日志22(自定义类型:联合和枚举)

目录 联合体 联合体类型的声明 联合体的特点 相同成员联合体和结构体的对比 联合体大小的计算 例子 枚举类型 枚举类型的声明 枚举类型的优点 枚举类型的使用 联合体 联合体类型的声明 像结构体一样,联合体也是由一个或者多个成员构成,这些成…

ROS2 Rviz 实战:给 panda 机械臂场景塞个圆柱体

视频讲解 ROS2 Rviz 实战:给 panda 机械臂场景塞个圆柱体 创建add_cylinder的package ros2 pkg create add_cylinder --build-type ament_cmake --dependencies rclcpp control_msgs moveit_ros_planning_interface 在src中添加add_cylinder.cpp,如下 #…

DeepSeek+知识库+鸿蒙,助力鸿蒙高效开发

不知道你们发现没有,就是鸿蒙开发官网,文档也太多太多了,对于新手来说确实头疼,开发者大多是极客,程序的目的是让世界更高效!看文档,挺头疼的,毕竟都是理科生。 遇到问题不要慌&…

从零开始在Windows使用VMware虚拟机安装黑群晖7.2系统并实现远程访问

文章目录 前言1.软件准备2. 安装VMware17虚拟机3.安装黑群晖4. 安装群晖搜索助手5. 配置黑群晖系统6. 安装内网穿透6.1 下载cpolar套件6.2 配置群辉虚拟机6.3 配置公网地址6.4 配置固定公网地址 总结 前言 本文主要介绍如何从零开始在Windows系统电脑使用VMware17虚拟机安装黑…

【LeetCode101】对称二叉树

题目描述 给你一个二叉树的根节点 root , 检查它是否轴对称。 思路与算法 对称:左右子树互为镜像 这很显然暗示了一种递归方法 确定base case(s) 如果 left 和 right 都是 None ,那么它们是镜像的(对称&…

K8s 1.27.1 实战系列(四)验证集群及应用部署测试

一、验证集群可用性 1、检查节点 kubectl get nodes ------------------------------------------------------ NAME STATUS ROLES AGE VERSION k8s-master Ready control-plane 3h48m v1.27.1 k8s-node1 Ready <none> …

【STM32】STM32系列产品以及新手入门的STM32F103

&#x1f4e2; STM32F103xC/D/E 系列是一款高性能、低功耗的 32 位 MCU&#xff0c;适用于工业、汽车、消费电子等领域&#xff1b;基于 ARM Cortex-M3&#xff0c;主频最高 72MHz&#xff0c;支持 512KB Flash、64KB SRAM&#xff0c;适合复杂嵌入式应用&#xff0c;提供丰富的…

pycharm找不到conda可执行文件

conda 24.9.2 在pycharm的右下角就可以切换python解释器了

自注意力机制的演进-从Transformer架构到DeepSeek-R1模型的深度语义理解革新

2025年&#xff0c;我国发布了开创性且高性价比的大语言模型-DeepSeek-R1&#xff0c;推动了AI领域的重大变革。本章节回顾了LLM的发展历程&#xff0c;其起点可追溯至2017年Transformer架构的提出&#xff0c;该架构通过自注意力机制(Self-Attention)彻底革新了自然语言处理技…

动态内存管理的了解及使用

目录 1.什么是动态内存&#xff1f; 2.为什么要使用动态内存分配空间&#xff1f; 3.动态内存开辟函数malloc&#xff0c;calloc&#xff0c;realloc 3.1 malloc 3.2 calloc 3.3 realloc 3.4 头文件包含 4.动态内存释放函数free 5.动态内存的基本常见错误 5.1 对NULL…

pom.xml配置(mybatisplus增删改查实现;PageInfo分页实现;JSONObject实现)

一、mybatisplus增删改查实现 pom.xml <!-- mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency>在mapper文件里…

【运维笔记】Navicat中删除mongo 某个时间之前的数据

【运维笔记】Navicat中删除mongo 某个时间之前的数据 一、场景与需求1.1、场景1.2、需求 二、解决方案三、实战3.1、【Navicat】使用sql语句 &#xff08;推荐&#xff09;Step 1&#xff1a;使用查询窗口 - 查询Step 2&#xff1a;确认第一步的数据是否是需要删除的数据Step 3…

BUUCTF逆向刷题笔记(1-12)

easyre、内涵的软件、xor、不一样的flag&#xff1a; buuctf reverse部分题解&#xff08;实时更新&#xff09;_reverse 题解-CSDN博客 请见小库里的blog。 reverse1 查壳发现没有&#xff0c;而且是64位 粗略改一下部分函数名&#xff0c;看看主要逻辑。 第一个for循环暂…

如何改变怂怂懦弱的气质(2)

你是否曾经因为害怕失败而逃避选择&#xff1f;是否因为不敢拒绝别人而让自己陷入困境&#xff1f;是否因为过于友善而被人轻视&#xff1f;如果你也曾为这些问题困扰&#xff0c;那么今天的博客就是为你准备的。我们将从行动、拒绝、自我认知、实力提升等多个角度&#xff0c;…

【DeepSeek】5分钟快速实现本地化部署教程

一、快捷部署 &#xff08;1&#xff09;下载ds大模型安装助手&#xff0c;下载后直接点击快速安装即可。 https://file-cdn-deepseek.fanqiesoft.cn/deepseek/deepseek_28348_st.exe &#xff08;2&#xff09;打开软件&#xff0c;点击立即激活 &#xff08;3&#xff09;选…

算法之二维装水问题

目录 1. 题目2. 解释3. 思路4. 代码5. 总结 1. 题目 给定一个数组arr&#xff0c;已知其中所有的值都是非负的&#xff0c;将这个数组看作一个容器&#xff0c;请返回容器能装多少水 比如&#xff0c;arr {3&#xff0c;1&#xff0c;2&#xff0c;5&#xff0c;2&#xff0c…

[C语言日寄] 字符串操作函数的使用及其拓展

【作者主页】siy2333 【专栏介绍】⌈c语言日寄⌋&#xff1a;这是一个专注于C语言刷题的专栏&#xff0c;精选题目&#xff0c;搭配详细题解、拓展算法。从基础语法到复杂算法&#xff0c;题目涉及的知识点全面覆盖&#xff0c;助力你系统提升。无论你是初学者&#xff0c;还是…

用Python的Pandas库解锁数据科学:从入门到实战

用Python的Pandas库解锁数据科学&#xff1a;从入门到实战 引言 Python的Pandas库&#xff08;名称源自"Panel Data"&#xff09;作为数据科学生态系统的基石&#xff0c;凭借其强大的数据结构和灵活的操作功能&#xff0c;已成为全球超过90%数据工作者的首选工具。…

MATLAB程序介绍,三维环境下的IMM(交互式多模型),使用CV和CT模型,EKF作为滤波

本文所述的MATLAB代码为三维的交互式多模型&#xff08;IMM&#xff09;滤波器&#xff0c;结合了匀速直线运动&#xff08;CV模型&#xff09;和匀速圆周运动&#xff08;CT模型&#xff09;的状态估计。使用扩展卡尔曼滤波&#xff08;EKF&#xff09;来处理状态更新与观测数…

[Windows] 多系统键鼠共享工具 轻松跨系统控制多台电脑

参考原文&#xff1a;[Windows] 多系统键鼠共享工具 轻松跨系统控制多台电脑 还在为多台电脑需要多套键盘鼠标而烦恼吗&#xff1f;是不是在操控 Windows、macOS、Linux 不同系统电脑时手忙脚乱&#xff1f;现在&#xff0c;这些问题通通能解决&#xff01;Deskflow 软件闪亮登…