结合java中的锁聊聊锁的本质

news2025/1/12 18:18:22
  • 在操作系统里面,也会遇到什么信号量、互斥量,然后说利用互斥量、信号量可以实现锁的功能,而操作系统提供的原语有又mutex锁

  • 在学习数据库的时候,什么表锁、行锁、读锁、写锁、排它锁、意向锁、meta锁等等,各种各样的锁的概念蜂拥而至

  • 在学习java的时候,我们会学习到synchronize关键字、jdk里的Lock接口,然后有各种各样的Lock的实现;然后在聊synchronize的时候又会又各种锁的概念出来:自旋锁、偏向锁、轻量级锁、重量级锁。

  • 在微服务架构下,大家又会遇到分布式锁,而分布式锁的实现有依赖于redis的、有依赖于zk的。然后又会聊到锁的锁的可重入、锁不能释放、错误释放等等等问题

  • 还有其他很多概念,比如乐观锁、悲观锁、读写锁、可冲入锁、不可冲入锁等等

那么到底什么是锁?这些各种各各样的锁到底是什么意思?下面我们就来聊聊锁的本质,以及不同地方的各种各样的锁到底是什么含义?

首先,锁的本质,其实就是一个标记,然后约定好:所有线程在进入临界区的时候,先去检查锁的状态,然后再根据锁的状态来决定当前线程该怎么办?

所以上面所有的锁的概念,都无外乎是根据这两个概念来对锁进行了分类:

  1. 锁标志:锁标志到底放在什么地方。mysql中的行锁、表锁无非就是锁标志是放在行维度还是表维度、而java中,不管是synchronize还是Lock接口,锁标志都是放在对象上的,所以从这个角度来讲,java的锁就是对象锁。

  1. 当遇到锁冲突的时候该怎么办?

  1. 如果当前线程只要遇到锁被占用,就直接进入到阻塞状态。那这个锁就是排它锁

  1. 如果当前线程只要遇到锁被占用,要看当前操作是什么,来决定到底是挂起、还是进入临界区,那这就是共享锁。进一步,如果按照读写来明确这个操作,那就是读锁、写锁、读写锁。

  1. 如果当前线程只要遇到锁被占用,不是立马进入到阻塞状态,而是不断去轮询一下锁的状态。这就是自旋锁。

  1. 如果当前线程只要遇到锁被占用,然后再看下将这个锁改成占用状态的是不是自己。如果是自己,那么就继续进入临界区执行;如果不是,再根据操作等来决定下一步动作。这就是可重入/不可重入锁。

  1. 如果当前线程只要遇到锁被占用,且自己也已经占用了一个锁标志,那么正好占用了当前线程站在检查的那个锁的标志的那个线程在检查当前线程占用的锁,就会出现线程的循环等待,那就是死锁

其他的一些锁相关的概念,很多其实都是一些优化手段,只是可能叫顺口了,也叫成了xxx锁,包括自旋锁其实也是一个优化动作。所以这里就不用去过多的care这些概念了。

java的synchronize锁

同样的,还是以两个方面来看java的synchronize的实现。

锁标志存放位置

synchronize使用的锁标志是存放在对象头里的。在java中,实例化一个独享后,除了对象里存放的数据外,编译器还会额外的给每个对象分配一块内存,存储一些对象数据,这块内存就是所谓的对象头。

java中,对象的对象头由三部分构成(如果是数组还有一部分就是记录数组长度的):

  1. 类型指针:类型指针指向的就是当前对象的类的元数据,虚拟机通过这个指针来确定该对象是哪个类的实例。

  1. mark word:这部部分主要用来记录该对象自身的运行时数据,如hashcode,gc分代年龄等。mark word占用一个jvm的字大小,即32位的jvm,mark word为32位;64位的jvm,mark word占64位。synchronize使用的锁标志,就是记录在mark word里面的。

以32位的jvm为例来说明mark word的构成

住院号就是两部分构成:

  1. 高30位部分:mark word中存放信息的部分

  1. 低2位部分:这部分就是标志位,低2位取值不同,代表了高30位存放的内容是不一样的

而当标志位=00、01、10的时候,高30位存储的就都是锁标志相关的信息。具体如下表格:

我们会发现,synchronize的锁标记,分了好多中情况,并不是只有一个占用 or 释放那么简单。通过低位的标志位区分了:无锁、偏向锁、轻量级锁、重量级锁,这写到底意味这什么?

遇到锁冲突怎么办

不管怎么样,我们已经明确了synchronize使用的锁标志是放在对象头的,那么第一个问题就明确了:锁的标志存放在哪里的

所以要了解无锁、偏向锁、轻量级锁、重量级锁这些概念是用来干啥的,区别是啥? 那就从第二个方面入手来分析:当前线程进入临界区的时候,检查锁标志的时候,发现锁标志占用,该怎么办?

如下这种图,就是synchronize执行过程中,加锁过程的大概流程:

其实我们会发现,这几个锁的区别无外乎就在于遇到了锁占用,当前线程该怎么办:

  1. 如果发现锁被占用,但是标志低2位是01,第3位是1,说明已经有线程获得过锁。这个时候,当前线程就CAS去改变搞30位存放的线程id,尝试把它改成自己:

  1. 如果成功了,那就变成自己获得锁。那么就去执行临界区代码。

  1. 如果执行失败了,说明原本持有锁的线程,还没有退出临界区。当前线程就不能进去临界区。所以这个时候咋办?当前线程就将对象头的低两位变成00,然后并且高30位的保存好原本获得偏向锁的线程信息(即让原来获得偏向锁的线程依然保持获得锁)。然后当前线程开启自旋CAS去修改搞30位的信息,试图将这些信息改成自己(即自旋CAS获得轻量级锁)

  1. 如果在最大自旋次数前,CAS成功了,那么说明原本获得锁的那个线程离开临界区了(因为离开临界区会来修改锁的标志),那么当前线程就直接进入临界区执行。

  1. 如果已经自旋到了最大次数限制了,CAS还没有成功。那么当前线程就将低2位的标志表变成10,高30依然保持原本获得锁的线程信息(即让原来获得锁的线程依然保持获得锁)。让后将自己挂起。

  1. 如果发现锁被占用,但是低两位是00,那就开始自旋CAS修改锁标志

  1. 如果发现锁被占用,但是低两位是10,那就直接将自己挂起。

为啥会搞这么复杂,其实java的第一个版本没这么复杂,简单粗暴理解即使,锁标志只有一个占用 or 空闲,当前线程遇到空闲,那么就将锁标志变成占用,自己进入临界区;当前线程遇到锁占用,就将自己挂起,进入这个锁的阻塞队列,状态变成阻塞状态,坐等原本占有所的线程退出临界区修改了锁标志后,唤醒,然后自己被唤醒后,就去判断锁标志。

可以发现,只要是发现锁占用,就会挂起当前线程、然后只要是离开临界区,就唤醒阻塞在这个锁的阻塞队列上的线程。而这些操作成本是比较大的。线程状态线程状态

  • 挂起线程,就会涉及用户态内核态的切换。我们知道cpu的这种航上下文切换是有成本的。

  • 唤醒线程,又会涉及用户态和内核态的来回切换。

  • 离开临界区唤醒所有线程,又会出现惊群问题。

那咋办呢,各位大佬经过一番研究统计后发现,在绝大部分使用锁的场景中,锁冲突并不严重,而且哪怕是遇到了锁冲突,占有锁的线程会很快释放。所以,那就想一些办法来避免挂起-唤醒的这一中操作,从而提高synchronize的效率。偏向锁、轻量级锁、自旋锁就应运而生了,

  1. 锁占用不是很快会释放么,那我遇到锁冲突了,我不是直接将自己挂起,我是空转等一会(具体实现其实就是不断CAS,因为cpu这段时间都在执行CAS,没有执行业务代码,所以认为是空转)。只要锁很快被释放,那当前线程就不用挂起自己啦,那么原本占有锁离开临界区的时候,也只需要修改锁标志,就不用唤醒所有线程了,那是不是自然就避免了挂起-唤醒那一堆成本呢?

到底什么情况才是自旋,而不是挂起呢?那就在标志上给个特殊的标记,只不过对外表达的时候,我们将锁标记=00的这种情况,称之为轻量级锁而已。

  1. 另外就是锁冲突概率不大么,那我就假设来竞争这个锁的线程是同一个线程,每次都优先将锁分配给同一个线程。那就出现了偏向锁。同样的,到底什么情况是执行偏向,而不是自旋、也不是挂起自己呢?所以也要给个特殊标记。

ps:我个人觉得偏向锁有点极端了,优化效果在实际生产中会不太理想,更不可接受的是,撤销偏向锁会STW,那就意味着业务耗时抖动。从jvm参数上,也可以看出,java发明这个功能的人也是不自信的,jvm参数中,是有不使用偏向锁的参数的,但是没有不是使用轻量级锁的参数吧。

所以简单粗暴点的理解,就是遇到偏向锁、轻量级锁、自旋锁冲突的时候,不是直接调用OS的系统调用,让当前线程进入阻塞的状态。所以其实就是synchronize实现时候的几个if分支罢了。

ps:实际实现的时候,锁的标志信息不一定完全都放在对象头,上面的表格也会发现,有些情况其实对象头里放的其实是个指针,这个指针指向的位置放了跟过的的关于锁的信息(比如可重入信息等),这个指针指向的位置,其实就是运行时栈。

jdk中的Lock

以比较常用的两类锁:可重入锁ReentrantLock和ReentrantReadWriteLock为例来说明:

同样的,我们还是从两方面来来说明会锁的实现:

  1. 锁标志存哪儿。在jdk中有一个锁实现的共有抽象父类,那就是AbstractQueuedSynchronizer,jdk中所有的锁实现都会依赖于这个抽象父类,这就是很多博客中锁说大名鼎鼎的AQS。这个类里就是用来存储锁标志的。比如AQS#state字段,就是来标记锁是否别占用的。其父类AbstractOwnableSynchronizer#exclusiveOwnerThread就是用来标记当前锁被哪个线程占用的,从而实现可重入。

  1. 当线线程在进入临界区时,遇到锁冲突怎么办? 使用Lock接口的时候,lock#lock()和lock#unlock()包围起来的就是临界区。所以任何一个线程要进入临界区都先要执行lock#lock()也是在这个方法里去检查锁标志的。

当锁被占用(AQS#state不为0),那么当前线程就会首先使用CAS去修改state值(即自旋获得锁),这个过程是和synchronize的轻量级锁的过程是一模一样的。无非就是synchronize的CAS是编辑器植入的,而Lock#lock()的CAS是用的UnSafe包装的CAS指令。

当CAS达到一定次数的时候(spinForTimeoutThreshold),就是使用LockSupport#park()将当前线程挂起。其实就是升级到重量级锁了。

这里也可以看到,和synchronize相比,jdk的实现,其实已经没有偏向锁了。只有轻量级锁和重量级锁

所以不管是synchronize还是Lock,其实锁标志 都是存放在对象上的,所以我们可以认为就是对象锁。所以来看下,如下这些情况锁标志都是放在哪儿的

class TestLock{
    public synchronized void doSomeThing(){
        // 业务逻辑
    }
}
class TestLock{
    Object lock = new Object();
    public synchronized void doSomeThing(){
        synchronized(lock){
              // 业务逻辑  
        }
    }
}
class TestLock{
    public void doSomeThing(){
        synchronized(TestLock.class){
             // 业务逻辑  
        }
    }
class TestLock {
    ReentrantLock lock = new ReentrantLock();
    public void doSomeThing() {
        lock.lock();
        try {
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    }
}

答案依次是:第一个是在this对象的对象头上;第二个是在Object lock这个指定对象的对象头上;第三个其实就是TestLock这个类加载到内存后为其创建的Class对象的对象头上;第四个当然就是ASQ上。

所以不管使用哪种方式,一定注意,锁防护的临界区,一定要用同一把锁(即锁标志是同一个对象上的)

ps:经典面试题,synchronize和Lock的区别。

其实以前老有人喜欢这么问,其本质还是在于synchronize以前(好像是jdk1.5以前)只有重量级锁,效率不高,所以拿这个作为一个点来问。

但是到了jdk1.8,其实synchronize已经做了很多优化了,其实效率是很高的了,所以这个时候又有人说了,区别在于灵活性,Lock接口更领过。但是synchronize也不是只能修饰方法,还是可以修饰代码块,且这个时候锁标志是放在指定对象的对象头上,其实这也比较灵活了,多个对象的方法使用同一把锁防护共享资源的访问,synchronize也是可以实现的。反而Lock,少补流程就有可能出错的,可能导致锁没有被释放的,所以使用Lock的时候一定要吧unlock放到finally块中,而synchronize却是编译器完成了这个事情,用户不用关系锁释放的事情的。

所以,synchronize唯一做不到的是:在A处加锁、在B处释放,而A、B两处是不同的代码块,但想想真实世界中,真有这么奇葩的使用方式么?

所以现在看起来,synchronize默认开启了偏向锁,偏向锁在有所的竞争的时候,其实会影响性能,如果关闭偏向锁,在我看来,这两者就只有使用语法和实现方式上的区别了。

但是,Lock确实更灵活,但他的灵活性体现在,他能够支持更多的条件变量,条件变量的使用其实是可以减缓惊群效应的。曾经也是硬总计了一些区别,可参考:lock和synchronize对比

mysql中的锁

mysql中的锁其实非常复杂,但是我们依然按照这两方面来认识mysql中各种锁。

首先还是看锁标志存放在哪儿?

  1. 如果锁的标志是存在库级别的,那就是库锁。

  1. 如果锁的标志是存在表级别的,那就是表锁。

  1. 如果锁的标志是存在行级别的,那就是行锁。

  1. 如果锁的标志是在行与行的间隙,那就是间隙锁(实际上msyql中不会有间隙这个数据结构的,但可以这么去理解)

  1. 如果锁的标志是存在shema级别的,那就是meta锁。

然后从遇到锁冲突改怎么办?

  1. 只要遇到锁冲突,那就挂起当前线程,这就是排它锁(或者叫写锁)。

  1. 遇到锁冲突,还有根据当前的操作来判断,是否挂起当前线程。就是共享锁、或者叫读锁

然后两者一结合,解释mysql中的各种锁,比如meta读锁、meta写锁、行上的读锁、行上年的写锁的能等,之所以有这么多所,无非就是mysql为了在不同场景去提交读写效率而已。

比如meta锁,每个写操作都会给表加meta读锁、但是meta读锁不会排斥写,但是修改表语句会给表加上meta写锁,那meta写锁就会阻塞所有的写操作了,这就是online ddl一直致力解决的问题,让修改表语句不要阻塞太多dml语句。

随着后续的管理控制粒度更细,可能还会冒出来更多的锁,但是它的实现,逃不开这两方面的。

总结:

所以,万变不离其宗,不管是哪个系统对锁的实现,我们都从这两方面去学习,会发现,都逃不过这两个方面,其实说白了,锁的本质就是一个标志,然后进入临界区的时候都去检查这标志,然后进去/退出临界区的时候修改标志,达到临界区同一时刻只会有一个线程来执行。

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

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

相关文章

mysql数据库limit的四种用法

文章目录前言一、语法二、参数说明三、常用示例-4种用法总结前言 mysql数据库中limit子句可以被用于强制select语句返回指定的记录数。limit接受一个或两个数字参数。参数必须是一个整数常量。如果给定两个参数,第一个参数指定第一个返回记录行的偏移量&#xff0c…

实践数据湖iceberg 第四十一课 iceberg的实时性-业界的checkpoint配置

系列文章目录 实践数据湖iceberg 第一课 入门 实践数据湖iceberg 第二课 iceberg基于hadoop的底层数据格式 实践数据湖iceberg 第三课 在sqlclient中,以sql方式从kafka读数据到iceberg 实践数据湖iceberg 第四课 在sqlclient中,以sql方式从kafka读数据到…

硬件系统工程师宝典(12)-----EMC应该知道的事

各位同学大家好,欢迎继续做客电子工程学习圈,今天我们继续来讲这本书,硬件系统工程师宝典。上篇我们说到在做电源完整性分析时去耦电容要遵循的规则,大电容的去耦半径大,小电容的去耦半径小,电容焊盘扇出时…

2023年java春招面试题及答案

2023年java春招面试题1、下面有关jdbc statement的说法错误的是?2、下面有关JVM内存,说法错误的是?3、下面有关servlet service描述错误的是?4、下面有关servlet和cgi的描述,说法错误的是?5、下面有关SPRIN…

Radio Link Monitoring(RLM)

欢迎关注微信同步公众号“modem协议笔记”。 这篇看下radio link monitoring相关的内容,就是UE进行DL radio link quality监听的规定,这部分与RLF的判定息息相关。市面上讲NR相关的书籍,多少都会涉及这部分内容,可能spec上这块的…

pdf免费转换工具,只需记住这3款就够了

PDF格式的文档在人们的办公过程中扮演着非常重要的角色,而PDF格式的文档之所以受到人们的青睐,是因为它不容易被篡改,可以用多种阅读器打开浏览。然而,在实际的应用过程中,我们不仅需要阅读PDF文档,也经常还…

HTTP压力测试概论

常用的HTTP服务压测工具介绍 在项目正式上线之前,我们通常需要通过压测来评估当前系统能够支撑的请求量、排查可能存在的隐藏bug,同时了解了程序的实际处理能力能够帮我们更好的匹配项目的实际需求,节约资源成本。 HTTP服务压力测试工具 在…

如何使用403bypasser绕过目标页面上的访问控制限制

关于403bypasser 403bypasser是一款自动化工具,该工具能够以自动化的形式实现针对目标页面的访问控制限制绕过技术。403bypasser项目目前仍处于积极开发阶段,并且还会增加新的功能。 该工具基于Python语言开发,因此具备良好的跨平台特性。 …

最好的 QML 教程,让你的代码飞起来!

想必大家都知道,亮哥一直深耕于 CSDN,坚持了好很多年,目前为止,原创已经 500 多篇了,一路走来相当不易。当然了,中间有段时间比较忙,没怎么更新。就拿 QML 来说,最早的一篇文章还是 …

基于云服务器的hexo博客搭建(稳)

文章目录序一、前置工具的准备域名购买及服务器购买二、搭建步骤1.配置服务器环境2.nginx 的配置4.git 及node.js安装1.安装node.js:2.安装Git及配置仓库1.安装git:5.本地主机配置(这里winr ,cmd,打开命令行操作)1.安装node.js2.安…

c++11 标准模板(STL)(std::unordered_set)(十二)

定义于头文件 <unordered_set> template< class Key, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator<Key> > class unordered_set;(1)(C11 起)namespace pmr { templ…

【计算机二级python】综合题目

计算机二级python真题 文章目录计算机二级python真题文本文件“红楼梦. txt”中包含了《红楼梦》小说前20章内容&#xff0c;“ 停用词. txt”包含了需要排除的词语。请修改模板&#xff0c;实现以下功能。‪‬‪‬‪‬‪‬‪‬‮‬‭‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‭‬‪…

【安卓】安卓设备实现wifi display解决方案

看文章前&#xff0c;我们需要知道的几个概念&#xff1a; 1、Wifi Direct技术&#xff1b; 2、Wifi Display技术&#xff1b; 3、Miracast标准&#xff1b; 安卓手机用户都知道我们的安卓手机有一个wifi直连功能&#xff0c;在点击设置–》WIFI–》更多Wifi设置–》Wifi直连&a…

回顾2-JAVA的标识符和关键词

JAVA的标识符和关键词 标识符概念: 所有地方都需要名称 如: 类的名字>类名 如:变量的名字,方法的名字,特殊关键词语的名字 这些符号就是一种标识, 涵盖一些意思 简单理解, 就是一种标记,识别,的符号关键词 这些词语, 在JAVA程序中, 被赋予了特殊的意义, 使用时候要避开同名…

编写SPI设备驱动程序

编写SPI设备驱动程序 文章目录编写SPI设备驱动程序参考资料&#xff1a;一、 SPI驱动程序框架二、 怎么编写SPI设备驱动程序2.1 编写设备树2.2 注册spi_driver2.3 怎么发起SPI传输2.3.1 接口函数2.3.2 函数解析致谢参考资料&#xff1a; 内核头文件&#xff1a;include\linux\s…

Python实现GWO智能灰狼优化算法优化BP神经网络回归模型(BP神经网络回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。1.项目背景灰狼优化算法(GWO)&#xff0c;由澳大利亚格里菲斯大学学者 Mirjalili 等人于2014年提出来的一种群智能优…

Shell高级——Linux中的文件描述符的本质(数组的下标)

以下内容源于C语言中文网的学习与整理&#xff0c;非原创&#xff0c;如有侵权请告知删除。 前言 Linux中一切接文件&#xff0c;比如 C 源文件、视频文件、Shell脚本、可执行文件等&#xff0c;就连键盘、显示器、鼠标等硬件设备也都是文件。 一个 Linux 进程可以打开成百上…

第15天-商城系统架构,首页渲染三级分类及搭建域名访问商城

1.商城系统介绍 1.1.技术方案 前后端分离服务端模板渲染&#xff08;选择&#xff09;商品详情页面静态化改造&#xff08;优化&#xff09; 1.2.动静分离架构 2.模板引擎Thymeleaf 2.1.Thymeleaf介绍 官方文档&#xff1a;https://www.thymeleaf.org/doc/tutorials/3.0/usin…

今天,小灰37岁了!

人们常常说&#xff0c;35岁是互联网人的中年危机。现在&#xff0c;小灰已经跨过了中年危机&#xff0c;倒不是因为小灰财务自由了&#xff0c;而是因为今天是小灰37岁的生日。年轻时候&#xff0c;小灰总觉得30岁是一个很遥远的年龄&#xff0c;而现在&#xff0c;小灰距离40…

KingbaseES V8R6 运维系列 --单机小版本升级

​ 案例说明&#xff1a; 在KingbaseES V8R6版本提供了sys_upgrade的升级工具&#xff0c; 本案例描述了KingbaseES V8R6单机环境下数据库的小版本升级操作&#xff0c;案例涉及的版本从‘(Kingbase) V008R006C005B0041’通过sys_upgrade升级到‘ (Kingbase) V008R006C005B0054…