Java并发系列之二:悲观锁机制

什么是锁

在并发环境下,会出现多个线程对同一个资源进行争抢的情况,假设A线程对资源正在进行修改,此时B线程此时又对资源进行了修改,这就可能会导致数据不一致的问题。为了解决这个问题,很多编程语言引入了锁机制,通过一种抽象的“锁”来对资源进行锁定,当一个线程持有“锁”的时候,其他线程必须等待“锁”,我认为这本质上就是在临界资源上对线程进行一种串行化。

Java语言中的锁机制是怎么设计的?在谈锁之前,我们需要简单了解一些Java虚拟机的内存结构。关于内存结构不是本文的重点,暂时不会影响到你的理解。我们可以来看这张图:

 

JVM运行时内存结构主要包含了五个部分:程序计数器(PC寄存器)、JVM栈、Native方法栈、 堆、方法区。

可以看到图中红色区域是各个线程私有的。这个区域中的数据,不会出现线程竞争的关系。而蓝色区域中的数据被所有线程共享,其中Java堆中存放的是大量对象,方法区中存放类信息、常量、静态变量等数据。当多个线程在竞争其中的一些数据时,可能会发生难以预料的异常情况。在程序开发中,锁的主要应用范围就是在数据共享区域。

了解了“锁”这种抽象的概念,那么在代码层面,它究竟是如何实现的?在Java中,主要采用了两种实现方式: 1. 基于Object的悲观锁。2. 基于CAS的乐观锁。本文主要讲解基于Object的悲观锁。

尝试用一句话概括:在Java中,每个Object,也就是每个对象都拥有一把锁,这把锁存放在对象头中,记录了当前对象被哪个线程占用。

对象、对象头

刚才提到了锁是存储在对象头中的,那么对象和对象头的结构分别是什么呢?

我们先来谈对象本身的结构,Java对象分为三个部分:

对象头

实例数据

对齐填充字节

TIPS:其中对齐填充字节是为了满足“Java对象大小是8字节的倍数”这一条件而设计的,为对象对齐填充了一些无用字节,大可不必理会。实例数据就是你在初始化对象时设定的属性和状态等内容。

对象头是我们这期要讲的重点之一,它存放了一些对象本身的运行时信息。对象头包含了两部分:

Mark Word

Class Pointer

相较于实例数据,对象头属于一些额外的存储开销,所以它被设计得极小(一般为232bit或264bit)来提升效率。Class Pointer是一个指针,指向当前对象类型所在方法区中的Class信息;Mark Word存储了很多当前对象的运行时状态信息,比如HashCode、 锁状态标志、指向锁记录的指针、偏向线程ID、锁标志位等等。

可以通过下面这张表对Mark Word有一个更直观的认识:

 

上面也提到了,对象头被设计得很小,Mark Word则主要体现了这一点,通过这张表我们可以看到,Mark Word只有32bit (或64bit) 并且它是非结构化的。这样,在不同的锁标识位下,不同字段可以重用不同的比特位,节省了空间。

我们从这张表中能看到,这把抽象的“锁”的信息就存储在对象头的Mark Word中。重点关注最后两位,这两位代表锁标志位,分别对应“无锁”、“偏向锁” 、“轻量级锁” 、“重量级锁”这四种状态。在Java中,启用对象锁的方式是使用、synchronizedi关键字,那么synchronized背后的原理是什么,上面列举的这些状态又都是什么意思呢?

synchronized

大家都知道在Java中,synchronized关键词可以用来同步线程,synchronized被编译后会生成monitorenter和monitorexit两个字节码指令,依赖这两个字节码指令来进行线程同步。

这里要介绍一样新事物: Monitor。Monitor常常被翻译成监视器或管程。关于Monitor,简单来说,你可以把它想像成一个只能容纳一名客人房间,而把想要获取对象锁的线程想像成想要进入这个房间的客人。一个线程进入了Monitor,那么其他线程只能等待,只有当这个线程退出,其他线程才有机会进入。

 

来看这张图,并模拟流程:

第一步:Entry Set中聚集了一些想要进入Monitor的线程,它们处于waiting状态。

第二步:假设某个名为A线程成功进入了Monitor,那么它就处于active状态。

第三步:此时A线程执行途中,遇到一个判断条件,需要它暂时让出执行权,那么它将进入wait set,状态也被标记为waiting。

第四步:这时entry set中的其他线程就有机会进入Monitor,假设一个线程B成功进入并且顺利完成,那么它可以通过notify的形式来唤醒wait set中的线程A,让线程A再次进入Monitor,执行完成后便退出。

这就是synchronized关键字所实现的同步机制,但是synchronized可能存在性能问题,因为monitor的下层是依赖于操作系统的Mutex Lock来实现的。Java线程事实. 上是对操作系统线程的映射(上篇讲到),所以每当挂起或唤醒一个线程都要切换到操作系统的内核态,这个操作是比较重量级的。在某些情况下,甚至切换时间本身就会超出线程执行任务的时间,这样的话,使用synchronized将会对程序的性能产生影响。

但是从Java6开始,synchronized进行了优化,引入了“偏向锁” 、“轻量级锁”的概念。因此对象锁总共有四种状态,从低到高分别是“无锁”、“偏向锁”、“轻量级锁”、“重量级锁”,这就分别对应了Mark Word中锁标记位的四种状态。

目前为止,我们已经搞懂了什么是锁,什么是对象头,Mark Word中的字段,synchronized、

monitor的初步原理,四种锁状态的由来。

接下来,你一定会对synchronized是如何优化的?这四种状态是如何变化的产生好奇,那么我们就来仔细盘一盘“无锁”、“偏向锁”、“轻量级锁”、“重量级锁”这四种状态各是什么。

对象锁的四种状态

无锁:

无锁顾名思义就是没有对资源进行操作系统级别(Mutex Lock)的锁定。在这个基础上,我理解“无锁”其实有两种语义。

第一种比较简单,某种资源不会出现在多线程环境下,或者说即使出现在多线程环境下也不会出现线程竞争的情况,那么确实无需对这个资源进行任何同步保护,直接让他给各个线程随意调用就可以了。在这里,指的是这种语意。

另一种情况,资源会被竞争,但是不使用操作系统同步原语对共享资源进行锁定,而是通过一些其他机制来控制同步。比如CAS,通过诸如这种函数级别的锁,我们可以进行“无锁”编程。顺便一提的是,上面也分析了依赖操作系统Mutex Lock导致性能低下的原因,所以在大部分情况下,无锁的效率更高,但这并非意味着无锁能够全面代替有锁。

偏向锁:

现在我们给对象开始加锁,假如一个对象被加锁了,但在实际运行时,只有一条线程会获取这个对象锁,那么我们最理想的方式,是不要通过系统状态切换,只在用户态把这件事做掉。我们设想的是,最好对象锁能够认识这个线程,只要是这个线程过来,那么对象就直接把锁交出去。我们可以认为这个对象锁偏爱这个线程,所以被称为“偏向锁”。

那么偏向锁是怎么实现的呢?其实很简单,在Mark Word中,当锁标志位是01,那么判断倒数第三个bit是否为1,如果是1,代表当前对象的锁状态为偏向锁,于是再去读Mark Word的前23个bit,这23个bit就是线程ID,通过线程ID来确认想要获得对象锁的线程是不是“被偏爱的线程”。

假如情况发生了变化,对象发现目前不只有一个线程,而是有多个线程正在竞争锁,那么偏向锁将会升级为轻量级锁。

轻量级锁:

当锁的状态还是偏向锁时,是通过Mark Word中的线程ID来找到占有这个锁的线程,那么当锁的状态升级到“轻量级锁”时,如何判断线程和锁之间的绑定关系呢?难道是通过不断的变更Mark Word中的线程ID的值?

非也,我们还是来看Mark Word这张表,这边已经不再使用线程ID这个字段,而是将前30个bit变为了指向虚拟机栈中锁记录的指针。

当一个线程想要获得某个对象的锁时,假如看到锁标志位为00,那么就知道它是轻量级锁,这时,线程会在自己的虚拟机栈中开辟一块被称为“Lock Record”的空间,关于虚拟机栈,上面简单讲过,是线程私有的。

Lock Record中存放什么呢?存放的是对象头的Mark Word的副本以及Owner指针。线程通过CAS去尝试获取锁,一旦获得,那么将会复制该对象的Mark Word到虚拟机栈的Lock Record中,并且将Lock Record中的Owner指针指向该对象锁。另一方面,对象的Mark Word中的前30bit将生成一个指针,指向持有该对象锁的线程虚拟机栈中的Lock Record。这样一来就实现了线程和对象锁的绑定,它们因此互相知道对方的存在。

这时,这个对象被锁定了,获取了这个对象锁的线程就可以去执行一些任务。那么你肯定要问,这时候万一有其他线程也想要获取这个对象,怎么办呢?此时其他线程将会自旋等待。什么叫“自旋”?你可以理解为一种轮询,其他想要获取对象锁的线程自己不断在循环尝试去看一下锁有没有被释放,如果被释放了,那么就获取,如果没有释放那么就进行下一轮循环,这种方式区别于被操作系统挂起阻塞,因为如果对象锁很快就会被释放的话,自旋去获得锁完全在用户空间解决,不需要进行系统中断和现场恢复,所以它的效率更高。

顺便提一下,自旋相当于是CPU在空转,如果长时间自旋,将会浪费CPU资源,于是出现一种叫做“适应性自旋”的优化,简单来说就是自旋的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的状态来决定。比如在同一个锁上,当前正在自旋等待的线程刚刚成功获得过锁,但是锁目前被其他线程持有,那么虚拟机就会认为下次自旋很有可能会再次成功,进而它将允许更长的自旋时间。这就是“适应性自旋”。

假如对象锁被一个线程持有着,此时也有一个线程正在自旋等待,如果同时又有多个线程想要获取这个对象锁。也就是说,一旦自选等待的线程数超过1个,那么轻量级锁将会升级为“重量级锁”。

重量级锁:

如果对象锁状态被标记为重量级锁,那么就和我最初讲的那样,需要通过Monitor来对线程进行控制,此时将会使用同步原语来锁定资源,对线程的控制也最为严格。

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

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

相关文章

前端学习--vue2--插槽

写在前面: 这个用法是在使用组件和创建组件中 文章目录 介绍简单使用多个插槽省写默认/后备内容作用域插槽常用实例Element-ui的el-table 废弃用法slot attributeslot-scope attribute 介绍 我们在定义一些组件的时候,由于组件内文字想要自定义&#…

ssh安全远程管理

目录 1、什么是ssh 2、ssh登陆 3、ssh文件传输 1、什么是ssh ssh是 Secure Shell 的缩写,是一个建立在应用层上的安全远程管理协议。ssh 是目前较为可靠的传输协议,专为远程登录会话和其他网络服务提供安全性。利用ssh 协议可以有效防止远程管理过程中…

机器学习笔记之优化算法(二)线搜索方法(方向角度)

机器学习笔记之优化算法——线搜索方法[方向角度] 引言回顾:线搜索方法从方向角度观察线搜索方法场景构建假设1:目标函数结果的单调性假设2:屏蔽步长 α k \alpha_k αk​对线搜索方法过程的影响假设3:限定向量 P k \mathcal P_k …

0基础学习VR全景平台篇 第76篇:全景相机-圆周率全景相机如何直播推流

圆周率科技,成立于2012年,是中国最早投身嵌入式全景算法研发的团队之一,亦是全球市场占有率最大的全景算法供应商。相继推出一体化智能屏、支持一键高清全景直播的智慧全景相机--Pilot Era和Pilot One,为用户带来实时畅享8K的高清…

PyTorch代码实战入门

人这辈子千万不要马虎两件事 一是找对爱人、二是选对事业 因为太阳升起时要投身事业 太阳落山时要与爱人相拥 一、准备数据集 蚂蚁蜜蜂数据集 蚂蚁蜜蜂的图片,文件名就是数据的label 二、使用Dataset加载数据 打开pycharm,选择Anaconda创建的pytorch环…

《工具箱-VNCServer》配置VNCServer,使用VNCViewer实现局域网内页面共享

VNCServer设置 通过VNCServer配置,与VNCviewer配套使用 1.下载并安装VNCServer 2.邮箱密码注册后用户登录 3.设置VNC密码 4.设置viewer不能控制本机 5.打开VNClicensewiz,选择“Enter a license key …” BQ24G-PDXE4-KKKRS-WBHZE-F5RCA BQ24G-PDXE4-…

Java中集合容器详解:简单使用与案例分析

目录 一、概览 1.1 Collection 1. Set 2. List 3. Queue 1.2 Map 二、容器中的设计模式 迭代器模式 适配器模式 三、源码分析 ArrayList 1. 概览 2. 扩容 3. 删除元素 4. 序列化 5. Fail-Fast Vector 1. 同步 2. 扩容 3. 与 ArrayList 的比较 4. 替代方案…

服务器介绍

本文章转载与b战up主谈三国圈,仅用于学习讨论,如有侵权,请联系博主 机架型服务器 堆出同时服务百万人次机组 刀型服务器 服务器炸了 比如用户访问量暴增 超过机组的峰值处理能力,进而导致卡顿或炸服, 适合企业的塔式…

同样是跨端框架,React会不会被VUE取代?

看到知乎上有比较多的类似问题,正好这两个框架在以往的一些项目中都有实践过,就借着本篇文章说说我个人的看法。 先摆个结论:不会,毕竟各有千秋,除非跨端框架有被更好的概念所替代,又或者App已经彻底过气了…

Pandas进阶修炼120题-第三期(金融数据处理,51-80题)

目录 往期内容:第一期:Pandas基础(1-20题)第二期:Pandas数据处理(21-50题) 第三期 金融数据处理51.使用绝对路径读取本地Excel数据方法一:双反斜杠绝对路径方法二:r 拓展…

【Docker】Docker安装Kibana服务_Docker+Elasticsearch+Kibana

文章目录 1. 什么是Kibana2. Docker安装Kibana2.1. 前提2.2. 安装Kibana 点击跳转:Docker安装MySQL、Redis、RabbitMQ、Elasticsearch、Nacos等常见服务全套(质量有保证,内容详情) 1. 什么是Kibana Kibana 是一款适用于Elasticse…

Java三大特征之继承【超详细】

文章目录 一、继承概念二、继承的语法三、父类成员访问3.1子类中访问父类的成员变量3.2子类和父类成员变量同名3.3子类中访问父类的成员方法 四、super关键字五、子类构造方法六、super和this七、再谈初始化八、protected 关键字九、继承方式十、final 关键字十一、继承与组合 …

C++学习day--18 空指针和函数指针、引用

1、void 类型指针 void > 空类型 void* > 空类型指针, 只存储地址的值,丢失类型,无法访问,要访问其值,我们必须对这个指 针做出正确的类型转换,然后再间接引用指针 。 所有其它类型的指针都可以隐…

郑州https数字证书

很多注重隐私的网站都注重网站信息的安全,比如购物网站就需要对客户的账户信息以及支付信息进行安全保护,否则信息泄露,客户与网站都有损失,网站也会因此流失大量客户。而网站使用https证书为客户端与服务器之间传输的信息加了一个…

python学到什么程度算入门,python从入门到精通好吗

本篇文章给大家谈谈python学到什么程度算入门,以及python从入门到精通好吗,希望对各位有所帮助,不要忘了收藏本站喔。 学习 Python 之 进阶学习 一切皆对象 1. 变量和函数皆对象2. 模块和类皆对象3. 对象的基本操作 (1). 可以赋值给变量(2). …

保护云数据库实用指南

在数字化转型时代,越来越多的企业将运营转移到云端,导致对云数据库的依赖越来越大。虽然它们提供了可扩展性和可访问性等显着优势,但它们也带来了独特的安全挑战,需要解决这些挑战以保护敏感数据免受各种威胁。 在本文中&#x…

【MybBatis高级篇】MyBatis 拦截器

【MybBatis高级篇】MyBatis 拦截器 拦截器介绍实现拦截器注册拦截器应用ymlDynamicSqlDao 层代码xml启动类拦截器核心代码代码测试 拦截器应用场景 MyBatis 是一个流行的 Java 持久层框架,它提供了灵活的 SQL 映射和执行功能。有时候我们可能需要在运行时动态地修改…

超细详解,接口自动化测试-JSON和JsonPath提取数据(实战)

目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 JSON(JavaScript …

kafka总结

Kafka是一种高吞吐量的分布式发布订阅消息系统(消息引擎系统),它可以处理消费者在网站中的所有动作流数据。 消息队列应用场景 缓存/削峰 :处理突然激增的大量数据,先放入消息队列,再按照速度去处理, 解…

Redis中的缓存雪崩、击穿、穿透的原因以及解决办法

redis的缓存 雪崩 击穿1.缓存雪崩双11访问很大,比如说redis设置缓存时间为3小时,当购物超过3小时之后 首页redis 在一瞬间全部失效,导致所有请求都打在db上.造成db在响应不及时直接就挂掉了 这个时候首页就不能立马对外响应服务了redis的key大面积失效 导致前端直接…