草庐IT

同步锁synchronized追本溯源

博学谷狂野架构师 2023-04-19 原文

1 同步锁synchronized追本溯源

引言
提到synchronized,无论是在开发过程中和面试过程中常常遇到的问题
synchronized;也算是重灾区了

为什么说是重灾区?
因为他不像其他的代码,是有源码,可以查看的
synchronized是一个关键字。直接是找不到源代码的

接下来
我们会通过java内存指令码和c++源码(HotSpot虚拟机源码)
给大家剖析一下synchronized到底是怎么实现锁同步的

1.1 synchronized场景回顾

目标:

synchronized回顾

概念

synchronized:是Java中的关键字,是一种同步锁。

syn属于哪种锁分类:

  • 乐观锁、悲观锁(syn)

  • 独享锁(syn)、共享锁

  • 公平锁、非公平锁(syn)

  • 互斥锁(syn)、读写锁

  • 可重入锁(syn)

tips:

synchronized JDK1.6锁升级 : 无锁 -> 偏向锁 (非锁)-> 轻量级锁 -> 重量级锁(1.6前都是)

多线程特性回顾(面试常问)

原子性:指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

可见性:是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。

有序性:指程序中代码的执行顺序 (编译器会重排)

sync可以完整实现以上三个特性来保障线程安全性,cas就无法达到原子性。

这是什么原理呢?

1.2 反汇编寻找锁实现原理

目标

通过javap反汇编看一下synchronized到底是怎么加锁的

com.syn.BTest

public class BTest {
    private static Object object = new Object();

     public synchronized void testMethod() {
        System.out.println("Hello World -synchronized method ");
    }

    public static void main(String[] args) {
        synchronized (object) {
            System.out.println("Hello World -synchronized block ");
            
        }
    }
}

反汇编后,我们将到什么?

JDK自带的一个工具: javap ,对字节码进行反汇编:

//com.syn.BTest 
javap -v -c BTest.class

-v:输出附加信息

-c:对代码进行反汇编

反汇编后

解释
被synchronized修饰的代码块,多了两个指令
monitorenter、monitorexit
即JVM使用monitorenter和monitorexit两个指令实现同步

解释

方法调用时会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。也就是jvm会隐式调用monitorenter和
monitorexit。

  • monitorenter原理

monitorenter首先我们来看一下JVM规范中对于monitorenter的描述

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

monitorexit: 
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译如下:

  • monitorenter

每一个对象都会和一个监视器monitor关联。

监视器被占用时会被锁住,其他线程无法来获取该monitor。

当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应
的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为
    monitor的owner(所有者)

  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1

  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直
    到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

  • monitorexit
  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。

  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出
    monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个
    monitor的所有权
    monitorexit释放锁。
    monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

tips(重要)

简单的理解,monitor就是jvm底层的c++代码中的一个对象ObjectMonitor。

这个对象里有个计数器,来记录当前对象锁有没有人用,用了多少次。

以及一些队列,存放调度一些需要这把锁的线程。

关于monitor在c++里的结构,我们下文再详细说。

总结:

1、synchronized是靠ObjectMonitor来控制锁的

2、需要这把锁的线程在monitor的队列里被各种安排

3、拿到锁的线程被monitor标记,计数加加,释放锁,需要将计数器减减操作

1.3 Monitor详解

目标:Monitor的位置

接下来我们看它的详细内部结构,以及如何运作的。

1.3.1 Monitor是什么

目标: 通过JVM虚拟机源码分析synchronized监视器Monitor到底是什么

tips:

c++源码了解即可,原理要明白

面试时很重要,面试过去了就不重要!(瞎说什么大实话)

在HotSpot虚拟机中,monitor监视器是由ObjectMonitor实现的。

构造器代码src/share/vm/runtime/objectMonitor.hpp

hpp可以include包含cpp的东西,两者都是c++的代码

//构造器
ObjectMonitor() {
  _header = NULL;
  _count = 0; 
  _waiters = 0,
  _recursions = 0; // 线程的重入次数
  _object = NULL; 
  _owner = NULL; // 当前线程,拿到锁的那位
  _WaitSet = NULL; // 等待队列,调wait的线程在这里
  _WaitSetLock = 0 ;
  _Responsible = NULL;
  _succ = NULL;
  _cxq = NULL; // 竞争队列,挣不到锁先进这里(可自旋)
  FreeNext = NULL;
  _EntryList = NULL; // 阻塞队列,来自cxq(调unlock时)或者waitSet(调notify时)
  _SpinFreq = 0;
  _SpinClock = 0;
  OwnerIsThread = 0;
}

留心这三个列表:

1)cxq(竞争列表)

cxq是一个单向链表。被挂起线程等待重新竞争锁的链表, monitor 通过CAS将包装成ObjectWaiter写入到列表的头部。为了避免插入和取出元素的竞争,所以Owner会从列表尾部取元素。所以这个东西可以理解为一上来竞争没拿到锁的在这里临时待一会(1级缓存)。

2)EntryList(锁候选者列表)

EntryList是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程,其他线程就待在里面。所以这个东西可以认为是二次竞争锁还没拿到的(里面有一个马上就会拿到)。(2级缓存)

备注:EntryList跟cxq的区别

在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。而EntryList中的线程都是被挂起的线程。

3)WaitList

WatiList是Owner线程地调用wait()方法后进入的线程。进入WaitList中的线程在notify()/notifyAll()调用后会被加入到EntryList。

过程总结:

  • 等待锁的线程会待在_cxq和entry set队列中,具体哪个和当前线程取锁的情况有关
  • entry set的表头线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为自己,同时monitor中的计数器_count加1
  • 若线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。
  • 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

1.3.2 详细流程图(了解)

monitorenter

monitorenter指令执行位置:

JVM源码:src/share/vm/interpreter/interpreterRuntime.cpp

JVM函数入口:InterpreterRuntime::monitorenter

最终调用:src/share/vm/runtime/objectMonitor.cpp中的 ObjectMonitor::enter

monitorexit

执行monitorexit指令位置:

代码文件:src/share/vm/runtime/objectMonitor.cpp

调用函数:ObjectMonitor::exit

本文由传智教育博学谷 - 狂野架构师教研团队发布
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力
转载请注明出处!

有关同步锁synchronized追本溯源的更多相关文章

  1. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  2. ruby-on-rails - 本地 yaml key 的 i18n 同步 - 2

    类似的问题,但对于java,Keepingi18nresourcessynced如何保持i18nyamllocals的key同步?即,当将key添加到en.yml时,如何将它们添加到nb.yml或ru.yml?如果我在my_title:"atitle"旁边添加键my_label:"sometextinenglish"我想把它给我的其他本地人我指定,因为我不能做所有的翻译,它应该回到其他语言的英语例如en.ymlsomegroup:my_tile:"atitleinenglish"my_label:"sometextinenglish"othergroup:...我想发出命令,将整个键和

  3. FIFO实战学习-同步FIFO/异步FIFO-格雷码 - 2

    目录FIFO一.自定义同步FIFO1.1代码设计1.2Testbech1.3行为仿真***学习位宽计算函数$clog2()***$clog2()系统函数使用,可以不关注***分布式资源或者BLOCKBRAM二.异步FIFO2.1在FIFO判满的时候有两种方式:2.2异步FIFO为什么要使用格雷码2.2.1介绍格雷码2.2.2格雷码在异步FIFO中的应用2.2.2格雷码判满2.4二进制与格雷码之间的转换2.4.1二进制码转换为格雷码的方法2.4.2格雷码转换为二进制码的方法2.3实现框图2.5实现及仿真代码2.6仿真图验证2.7结论FIFO  这篇更多的是记录FIFO学习,参考了众多优秀的文章,

  4. ruby - 如何在 Capybara 中使用 synchronize? - 2

    如果如何使用wait_until非常清楚(我在通过nativeWebdriver方法创建测试时使用过这样的方法),但不是新的同步方法(抱歉:))。我已经阅读了关于为什么不推荐使用wait_until的主题,我已经阅读了相关文章,我已经阅读了带有方法描述的文档,还阅读了描述中的代码。但我没有找到任何示例或教程如何使用此方法。任何人,请提供一些我(也许还有其他人)可以看到并学习如何使用此方法的案例例如案例expect(actual).toequal(expected)我应该在哪里“放置”同步方法以仅在超时后才获得否定异常?UPD:有兴趣的请查看此链接:http://www.elabs.se

  5. DolphinScheduler 调度 DataX 实现 MySQL To ElasticSearch 增量数据同步实践 - 2

    数据同步的方式数据同步的2大方式基于SQL查询的CDC(ChangeDataCapture):离线调度查询作业,批处理。把一张表同步到其他系统,每次通过查询去获取表中最新的数据。也就是我们说的基于SQL查询抽取;无法保障数据一致性,查的过程中有可能数据已经发生了多次变更;不保障实时性,基于离线调度存在天然的延迟;工具软件以Kettle(ApacheHop最新版)、DataX为代表,需要结合任务调度系统使用。基于日志的CDC:实时消费日志,流处理,例如MySQL的binlog日志完整记录了数据库中的变更,可以把binlog文件当作流的数据源;保障数据一致性,因为binlog文件包含了所有历史变更

  6. ruby - ruby 是否具有与 synchronize 关键字等效的 Java? - 2

    ruby是否有Java中的synchronize关键字?我使用的是1.9.1,但我不太明白执行此操作的优雅方式。 最佳答案 它没有synchronize关键字,但您可以通过Monitor类获得非常相似的东西。以下是ProgrammingRuby1.8一书中的示例:require'monitor'classCounter 关于ruby-ruby是否具有与synchronize关键字等效的Java?,我们在StackOverflow上找到一个类似的问题: http

  7. ruby - Ruby 中用于并发的 Synchronized 方法 - 2

    这个问题在这里已经有了答案:关闭10年前。PossibleDuplicate:DoesrubyhavetheJavaequivalentofsynchronizekeyword?在Java中,我们可以通过在函数定义中使用“synchronized”关键字来使方法“同步”。我们如何在Ruby中做到这一点?

  8. IGH主站通信测试csp模式(DC同步 preemrt)连通一从站并实现控制 - 2

    IGH主站通信测试linuxcnc配置基础机器人控制LinuxCNC与EtherCAT介绍&&PDO&SDO,搭建环境步骤需要配置IGH主站的查看这篇文章linux系统学习笔记7——一次性安装igh-ethercat主站CSP模式DC同步方式preemrt实时补丁直接上代码,这部分是直接控制使用csp模式控制一个从站运动使能后直接运动,10s,每秒607a(目标位置)增加100.注意:急停按下ESC代码分为两部分,一个是通信线程主要负责和伺服通信,使能伺服,读取和写入寄存器值。第二个是操作线程,负责修改位置的值,和监控按键。使用此代码,首先根据手册1.修改PDO条目,要和自己的伺服一致2.修改

  9. javascript - 为什么 org/arangodb/request 是同步的? - 2

    为什么新的JavaScript模块request同步?它应该只用于作业队列吗?有什么方法可以在ArangoDB中发出异步http(s)请求吗? 最佳答案 完全披露:我是ArangoDB开发团队的一员,主要从事Foxx和所有JavaScript方面的工作。我也是写org/arangodb/request的人模块。ArangoDB是一个不同于Node.js的环境,尽管有许多相似之处(例如使用V8JavaScript引擎)。与Node.js(或浏览器)不同,ArangoDB使用基于线程的并发模型并且没有事件循环。然而,线程并没有在Java

  10. javascript - 本地修改数据的 Firebase 同步 : handling errors & global status - 2

    我有两个关于Firebasewebplatform的相关问题的synchronisationoflocally-modifieddatatotheserver:EveryclientsharingaFirebasedatabasemaintainsitsowninternalversionofanyactivedata.Whendataisupdatedorsaved,itiswrittentothislocalversionofthedatabase.TheFirebaseclientthensynchronizesthatdatawiththeFirebaseserversandw

随机推荐