synchronized 使用及实现原理

synchronized 关键字

如何使用

synchronized 关键字的使用方式主要有下面 3 种:

  1. 修饰实例方法

  2. 修饰静态方法

  3. 修饰代码块

1、修饰实例方法 (锁当前对象实例)

给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

synchronized void method() {
    //业务代码
}

此时,synchronized加锁的对象就是这个方法所在实例的本身。

2、修饰静态方法 (锁当前类)

给当前类加锁,会作用于这个类的所有对象实例

这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {
    //业务代码
}

3、修饰代码块 (锁指定对象/类)

对括号里指定的对象/类加锁:

  • synchronized(object) 给对象加锁

  • synchronized(类.class) 给类加锁

synchronized(this) {
    //业务代码
}

 


synchronized关键字是如何对一个对象加锁实现代码同步的呢?

synchronized原理

synchronized 底层实现原理

底层原理就是,通过monitorenter 和 monitorexit 来完成同步机制,所以我们不用通过 lock 和 unlock 实现上锁和释放锁的过程。

当 synchronized 修饰方法的时候,JVM 采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。

synchronized修饰同步方法


在JVM中,对象在内存中存储的布局可以分为三个区域,分别是对象头、实例数据以及填充数据。

  • 实例数据 存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐。

  • 填充数据 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

  • 对象头 在HotSpot虚拟机中,对象头又被分为两部分,分别为:Mark Word(标记字段)、Class Pointer(类型指针)。如果是数组,那么还会有数组长度。对象头是本章内容的重点,下边详细讨论。

对象头

在对象头的Mark Word中主要存储了对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID以及偏向时间戳等。同时,Mark Word也记录了对象和锁有关的信息。

当对象被synchronized关键字当成同步锁时,和锁相关的一系列操作都与Mark Word有关。Mark Word在不同锁状态下存储的内容有所不同。

GC标记是垃圾回收机制

可以看到重量级锁对象头的MarkWord中存储了指向Monitor对象的指针,那么什么是Monitor?

Monitor对象

Monitor对象被称为管程或者监视器锁。在Java中,每一个对象实例都会关联一个 Monitor 对象。这个Monitor对象既可以与对象一起创建销毁,也可以在线程试图获取对象锁时自动生成。当这个Monitor对象被线程持有后,它便处于锁定状态。

Monitor是由ObjectMonitor实现的,它是一个使用C++实现的类,主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 调用wait方法后的线程会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; // 阻塞队列,线程被唤醒后根据决策判读是放入cxq还是EntryList
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 没有抢到锁的线程会被放到这个队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
 

ObjectMonitor中有五个重要部分,分别为ower,WaitSet,cxq,EntryList和count。

  • _ower 用来指向持有monitor的线程,它的初始值为NULL,表示当前没有任何线程持有monitor。当一个线程成功持有该锁之后会保存线程的ID标识,等到线程释放锁后_ower又会被重置为NULL;

  • _WaitSet 调用了锁对象的wait方法后的线程会被加入到这个队列中;如果调用锁对象的wait()方法,线程会释放当前持有的monitor,并将owner变量重置为NULL,且count减1,同时该线程会进入到_WaitSet集合中等待被唤醒。

  • _cxq 是一个阻塞队列,线程被唤醒后根据决策判读是放入cxq还是EntryList;

  • _EntryList 没有抢到锁的线程会被放到这个队列;

  • count 用于记录线程获取锁的次数,成功获取到锁后count会加1,释放锁时count减1。

问题:synchronized 锁住的到底是什么?

答:是 monitor enter 和 monitor exit

MarkWord 中存储了指向 Monitor 对象的指针,Monitor 是由ObjectMonitor 实现

ObjectMonitor 原理

当获取 monitor 对象的线程进入 _ower 区的时候, _count+1。当调用 wait()方法后,释放 monitor 对象, _count - 1,退出 _ower 区。

当对象的Monitor的计数器count为0 的时候,线程可以取得Monitor,并将count设置为1,即获得该对象的锁,表示只有这个线程可以对该对象进行操作。如果当前线程已经拥有该对象monitor的持有权,那它可以重入这个 monitor ,计数器的值也会加 1。而当执行monitorexit指令时,锁的计数器会减1。

如果对象的Monitor的count为1时,那么当前线程获取锁失败将被阻塞并进入到_EntryList中,直到等待的锁被释放为止。也就是说,当所有相应的monitorexit指令都被执行,计数器的值减为0,执行线程将释放 monitor(锁),其他线程才有机会持有 monitor 。

流程

执行 monitorenter 获取锁

所以当了解完原理后,就能知道 monitor 是与同步有关系,所以synchronized 锁住的是:

  • monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。

  • monitorexit,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。

锁的升级

无锁->偏向锁->轻量级锁->重量级锁

锁只能由这个方向进行升级,不可以逆着来。

偏向锁

由于大部分情况下啊,一般都是同一线程反复获得锁,每一次获得锁的过程中,需要获得锁和解锁的操作,所以,如果只有一个线程的话,那可以创建一种锁,当线程获得锁之后,就不进行解锁操作了,每次这个线程进入或退出同步快的时候,只需要比较下线程id就可以了

偏向锁加锁

当一个线程访问同步块的时候(同一时刻只有一个线程能执行同步块之中的代码),会在对象头和栈帧中存储锁偏向的线程id,如果该线程再进入的时候,只需要比较下线程id就可以。

偏向锁撤销
  • 如果原来持有偏向锁线程已经 退出同步代码块或者已经死亡,那么偏向锁可以直接撤销,转变为无锁状态。

  • 如果还在代码块之中,那么偏向锁升级为轻量级锁,原来的线程仍持有锁,其他进程需要cas,来竞争锁

优点

保证在只有一个线程的情况下,线程获得锁之后,该线程进入或退出同步代码块不需要进行cas操作,如果有多个锁竞争,那么锁会进行升级。

轻量级锁

轻量级锁是一种Java中实现线程同步的锁,它比重量级锁更高效,但也需要一定的条件才能使用。轻量级锁的加锁和撤销过程如下:

加锁过程

当一个线程要执行同步代码块时,它会先在自己的栈帧中创建一个锁记录,然后把对象头中的Mark Word复制到锁记录中,这叫做Displaced Mark Word。接着,线程会用CAS操作尝试把对象头中的Mark Word替换成指向锁记录的指针。如果成功,说明线程获得了轻量级锁,可以继续执行同步代码块。如果失败,说明有其他线程也在竞争这个锁,那么当前线程就会进行自旋,即不断重试CAS操作,直到成功或者超过一定次数。

撤销过程:

当一个线程执行完同步代码块时,它会用CAS操作尝试把对象头中的Mark Word恢复成原来的Displaced Mark Word。如果成功,说明没有其他线程竞争该锁,轻量级锁就被释放了。如果失败,说明有其他线程在自旋等待该锁,那么当前线程就会通知其他线程停止自旋,然后把锁升级为重量级锁,释放该锁后唤醒一个等待的线程。

具体CAS过程

CAS(Compare And Swap)是一种原子操作,它可以保证多个线程同时对同一个内存地址进行更新时的正确性。CAS操作需要三个参数:内存地址、期望值和新值。

CAS操作的过程是这样的:

  • 首先,从内存地址中读取当前值,与期望值进行比较,如果相等,说明没有其他线程修改过该内存地址,那么就用新值替换当前值,并返回成功。

  • 如果不相等,说明有其他线程修改过该内存地址,那么就放弃替换,并返回失败。

在轻量级锁的加锁和撤销过程中,CAS操作的内存地址就是对象头中的Mark Word,期望值就是Displaced Mark Word,新值就是指向锁记录的指针或者原来的Displaced Mark Word。CAS操作可以保证只有一个线程能成功获取或释放轻量级锁,其他线程则需要自旋或者阻塞。

轻量级锁优缺点

轻量级锁的优点是可以避免线程的阻塞和切换,因为得不到锁的线程不会被挂起,而是进行自旋,如果锁释放,可以第一时间知道,提高程序的响应速度。轻量级锁的缺点是如果一直不能获取到锁,长时间的自旋会造成CPU消耗。轻量级锁适用于少量线程竞争锁对象,且线程持有锁的时间不长,追求响应速度的场景。

总结

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ViewModel 完全指南:实践与背后原理全解

一、引言 在现代Android应用开发中,处理UI数据的有效管理和状态保持是开发者面临的重要挑战之一。Google推出的Jetpack组件库中的ViewModel已成为解决这些问题的关键工具。ViewModel旨在以生命周期意识的方式存储和管理界面相关的数据,从而使数据在配置…

暴力法解决最近对问题和凸包问题-实现可视化

目录 最近对问题 凸包问题 最近对问题 顾名思义就是采用蛮力法求出所有点之间的距离,然后进行比较找出第一个最近对,一个一个进行比较。 大概思路就是如图(每个圈代表一个数对) 第一个和其他四个比较 第二个和其他三个比较 …

C++类和对象下——实现日期类

前言 在学习了类和对象的六大成员函数后,为了巩固我们学习的知识可以手写一个日期类来帮助我们理解类和对象,加深对于其的了解。 默认函数 构造函数 既然是写类和对象,我们首先就要定义一个类,然后根据实际需要来加入类的数据与函…

计算机Java项目|Springboot房产销售系统

作者主页:编程指南针 作者简介:Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容:Java项目、Python项目、前端项目、人工智能与大数据、简…

Windows下安装Node.js、npm和electronic,并运行一个Hello, World!脚本程序

20240510 By wdhuag 目录 简介: 参考: 安装Node.js 安装npm 配置npm: 修改包存放目录和缓存目录 切换镜像源 使用 nrm 切换镜像源 安装Electron 运行一个Hello, World!脚本程序 安装Yarn JavaScript 指南 简介: Nod…

flash attention的CUDA实现探讨-V3

之前关于flash attention的实现参考添加链接描述,添加链接描述,添加链接描述 lash attention的数学变换:给定三个矩阵Q,K,V,形状都是[N,d],计算S=QK.T,然后针对dim=1做softmax,然后和V继续做矩阵乘法得到形状为[N,d]的输出矩阵O,即O=softmax(QK.T,dim=1)V。 下面本人的…

物联网设计竞赛_2_Jetson Nano中文输入法配置安装vscode

1、装ibus和ibus依赖框架 sudo apt-get install ibus ibus-clutter ibus-gtk ibus-gtk3 ibus-qt4 2、启动ibus im-config -s ibus 3、安装拼音引擎 sudo apt-get install ibus-pinyin 4、重启linux系统 shutdown -r now 5、进入ibus设置添加中文 ibus-setup 插入中文p…

四川汇昌联信:拼多多网点怎么开?大概需要多少钱?

想要开一家拼多多网点,你肯定很关心需要准备多少资金。下面,我们就来详细解答这个问题,并从多个角度分析开设网点的要点。 一、 开设拼多多网点,首要任务是确定启动资金。根据不同的经营模式和地区差异,成本会有所不同…

基于SpringBoot + Vue的兼职网站管理系统设计与实现+毕业论文+答辩PPT

系统介绍 本系统包含管理员、用户、企业三个角色。 管理员角色:前台首页、个人中心、用户管理、企业管理、兼职信息管理、职位申请管理、留言板管理、系统管理。 用户角色:前台首页、个人中心、职位申请管理。 企业角色:前台首页、个人中心、…

JUC下的ThreadLocalRandom详解

ThreadLocalRandom 是Java并发包(java.util.concurrent)中提供的一个随机数生成器类,它是从Java 7开始引入的。相较于传统的Math.random()或Random类,ThreadLocalRandom更适用于多线程环境,因为它为每个线程维护了一个…

【spring】application.yml导入额外配置文件

有时候application.yml 已经配置很多配置已经很大很乱了想把他们拆出去放在一个独立的XX.yml文件管理。这时候就用到了 spring.config.import 属性。 spring.config.import spring.config.import 是 Spring Boot 2.4 版本引入的一个配置属性,用于导入额外的配置数…

Java入门基础学习笔记21——Scanner

在程序中接收用户通过键盘输入的数据: 需求: 请在程序中,提示用户通过键盘输入自己的姓名、年龄、并能在程序中收到这些信息,怎么解决? Java已经写好了实现程序,我们调用即可。 API:Applicat…

解决 XXL-Job 端口额外占用问题 小结

🏷️个人主页:牵着猫散步的鼠鼠 🏷️系列专栏:Java技术栈笔记 🏷️个人学习笔记,若有缺误,欢迎评论区指正 目录 1. 前言 2. 问题解决 2.1. 下载源码 2.2. 启动admin服务器 2.3. 项目引入c…

MySQL-InnoDB数据存储结构

1、存储结构-页 索引结构提供了高效的索引方式,索引信息以及数据记录都保存在数据文件或索引文件中(本质存储在页结构中) 1.1、磁盘与内存交互的基本单位:页 在InnoDB中将数据划分为若干页,页的默认大小为&#xff…

OpenCV-基于累计直方图的中值滤波算法

作者:翟天保Steven 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处 实现原理 基于累计直方图的中值滤波算法是一种图像处理技术,用于去除图像中的噪声。它利用了像素值的频数分布&#…

LeetCode/NowCoder-链表经典算法OJ练习2

最好的,不一定是最合适的;最合适的,才是真正最好的。💓💓💓 目录 说在前面 题目一:分割链表 题目二:环形链表的约瑟夫问题 SUMUP结尾 说在前面 dear朋友们大家好!&…

卷积神经网络边缘识别

为什卷积神经网络能够识别图片呢?是基于图片相似度比较,两张图片的点击越大说明两张图片越像,比如我们那狗胡子的图片去比较,如果相似度很高,就是认为这个动物更像狗。点积越大,图片越相似,这个…

Windows2016系统禁止关闭系统自动更新教程

目录 1.输入cmd--适合系统2016版本2.输入sconfig,然后按回车键3.输入5,然后按回车键4.示例需要设置为手动更新,即输入M,然后按回车键 1.输入cmd–适合系统2016版本 2.输入sconfig,然后按回车键 3.输入5,然后…

揭秘APP广告变现:轻松赚取收益的秘密武器,你还在等什么?

在移动互联网时代,APP广告变现已成为许多开发者和公司获取收益的重要方式。它如同一把秘密武器,帮助那些掌握了其使用技巧的人轻松赚取收益。那么,究竟什么是APP广告变现?又如何通过它轻松赚取收益呢?接下来&#xff0…

对中介者模式的理解

目录 一、场景1、题目 【[来源](https://kamacoder.com/problempage.php?pid1094)】1.1 题目描述1.2 输入描述1.3 输出描述1.4 输入示例1.5 输出示例 二、不采用中介者设计模式1 代码2 问题 三、中介者设计模式1 代码2 更好的例子 四、个人思考 一、场景 设计模式不是银弹&am…