【Java 并发编程】(二) 从对象内存布局开始聊 synchronized

对象的内存布局

首先抛出一个经典面试题: 一个 Object 对象占多大?
在这里插入图片描述

这里我用工具打印了出来, 发现是 “16bytes”, 也就是 16B; 为什么? 请继续往下看;

在这里插入图片描述

普通对象(除了数组), 由markword, 类型指针, 实例数据(就是对象里的成员), 对齐填充(整个对象大小要能被8B整数, 方便64bit总线)构成;

  • markword 中保存了监视器锁的信息, GC 的信息, 还可能保存默认的 hashCode 方法计算出的哈希值;

  • 类型指针指向这个对象所属的类的类对象;

  • java命令默认带两个压缩指针的参数, UseCompressedClassPointersUseCompressedOops(Ordinary Object Pointers), 在64位环境下, 一个指针应当占8字节; 第一个指令会将对象头中的类型指针压缩位4B, 第二个参数会将成员指针压缩为4B;

  • 一个java应用所占的内存大小, 几乎不会大到需要用 64 位的寻址空间, 32 位完全够了, 这是为什么可以开启指针压缩;

  • 对于Object类, markword 8B, 类型指针4B, Object类型没有实例数据0B, 对齐填充4B, 总共 16B

对象头

在这里插入图片描述

  • markword中记录了锁信息, GC信息(例如当前熬过了几次垃圾回收, 注意这个字段只有4bit最大值15)
  • 如果使用了默认的hashcode方法, 那么计算出来的 hash 值会被放到markword里保存; 而这个保存的地方, 和偏向锁是有冲突的, 如果调用过默认的 hashCode 方法, 偏向锁就不能用了;
  • 使用了偏向锁, 再调用默认的 hashCode, 会触发锁升级;

监视器锁

特点

  1. 对象级别的锁: 监视器锁是与对象关联的,每个对象都有一个内置的监视器锁。当一个线程获取了对象的监视器锁后,其他线程就无法同时获取该对象的监视器锁,直到锁被释放。

  2. 排他性访问: 监视器锁实现了排他性访问,即同一时刻只有一个线程能够持有对象的监视器锁,其他线程需要等待。

  3. 基于进入和退出监视器锁的规则: 线程可以进入同步代码块或同步方法,获取对象的监视器锁;当线程退出同步代码块或同步方法时,会释放对象的监视器锁。

  4. 可重入性: 监视器锁支持线程的可重入性,即同一个线程在持有锁的情况下可以重复地继续获取锁,而不会发生死锁。但是要注意获取多少次锁就需要释放多少次锁

  5. 等待和唤醒机制: 一个线程持有监视器锁时, 其它线程尝试获取锁时将被阻塞, 放到该锁的阻塞队列中; 持有锁的线程释放锁后, 如果有其他线程正在等待获取同一个对象的监视器锁,那么其中的一个线程会被唤醒;

Synchronized

synchronized代码块

synchronized(对象){
    ...
}
  • 使用同步代码块的线程执行时如果出现异常, 会自动释放锁;
  • 小括号内的对象又称互斥条件, 是同步代码块锁住的对象; 可以使用this, 某个特定类的对象, 某个类的类对象;
  • 当线程进入 synchronized 代码块时,它会尝试获取对象的监视器锁。如果对象的监视器锁已经被其他线程持有,则当前线程会被阻塞,直到获取到锁为止。获取到锁后,线程就可以执行 synchronized 代码块中的代码,直到代码块执行完毕或者抛出异常,然后释放锁。
  • 具有可重入的特点;

synchronized方法

  • synchronized关键字加在静态方法上, 则进入方法时会尝试获取该静态方法所属的类的类对象的监视器锁;
  • synchronized加在成员方法上, 进入方法时会尝试获取调用该方法的对象的的监视器锁;
  • 退出方法时释放锁;

synchronized实现原理

  1. 源码中, synchronized关键字
  2. 编译后变为对监视器锁的操作, monitor enter 和 monitor exit, 实际就是对对象头中markword的修改
  3. 在JVM执行过程中, 会自动进行锁升级
  4. 最终依靠汇编指令, lock_cmpxchg指令实现

非公平

  1. synchronized是非公平锁, 当一个线程尝试获取锁时, 不会优先让阻塞队列中的线程获取锁; 而是一上来自己就尝试获取锁;
  2. 通常是唤醒队头元素, 通常也就是等待时间最久的元素

wait / notify

  • 需要在synchronized代码块内部或者synchronized方法内部, 已经获取了某个对象的监视器锁的前提下, 才能用这个对象去调用wait/notify方法

  • wait:

    • 调用之前必须先获取到该对象的监视器锁
    • 某个对象调用wait方法后, 当前线程会释放这个对象的监视器锁, 并进入 WAITING 状态, 加入到该监视器锁的waiting队列中, 等待其他线程调用相同对象的notify/ notifyAll方法唤醒;
  • notify:

    • 调用之前必须先获取到该对象的监视器锁
    • 调用notify方法后会在该监视器锁的waiting队列中唤醒一个线程;
    • 被唤醒的线程会重新尝试获取该对象的监视器锁; 获取到以后才能继续执行; 获取失败就blocked
  • notifyAll:

    • 与notify的区别是会唤醒监视器锁waiting队列中的所有线程

为什么要在获取监视器锁以后才能 wait 和 notify ? 主要是为了防止一次 notify 调用导致多个 WAITING 状态的线程被唤醒; 在 synchronized 代码块内部, 唤醒时需要重新获取监视器锁, 避免多个线程能同时往下执行;

锁升级

  • synchronized 获取对象的锁时, 获取方式会根据线程竞争情况进行升级; 因为重量级锁是要向操作系统申请的, 时间消耗比较大;

  • 在Java中,锁的状态包括偏向锁、轻量级锁和重量级锁。

  • 偏向锁: 当一个线程第一次进入同步块时,会尝试获取对象的偏向锁, 本质就是通过 CAS(下文会有详细介绍) 的方式, 将指向本线程的指针, 写到对象头里; 以后相同线程再尝试获取相同对象的监视器锁时, 会直接获取成功;

  • 轻量级锁: 当有不同线程尝试获取锁时, 锁状态升级为轻量级锁, 轻量级锁使用CAS操作来尝试获取锁。和获取偏向锁类似, 用CAS操作尝试将自己的 LockRecord 指针写入对象头来实现获取锁; 每个线程有自己的方法栈, 方法栈里有LockRecord; 如果获取失败,线程就会进行自旋操作继续尝试, 自旋达到一定次数后(由JVM动态计算此阈值),轻量级锁会升级为重量级锁;

    为什么变成用 LockRecord了? 接着用线程指针不行吗? 应该需要保存一些额外的信息了; 比如说自旋次数;

  • 重量级锁: 将获取锁失败的线程阻塞;

    重量级锁是向操作系统申请获取的, 重量级锁的操作都是由操作系统负责的;

    当某个线程获取重量级锁失败时, 会进入该锁对应的阻塞队列中;

    当临界区大, 并发级别高时, 适合使用重量级锁, 避免cpu资源的消耗;临界区小, 并发级别低时, 适合使用轻量级锁

  • 补充两个概念, 知道就行

    锁粗化

    JVM检测到一连串的操作都是对同一个对象进行加锁的话, 会将锁的范围粗化到一系列操作的外部, 只进行一次加锁;

    StringBuffer是线程安全的, append方法需要加锁;

    while(i < 100){
    i++;
    sb.append("hello");
    }
    

    锁消除

    当确定某个锁不必要的时候, 例如一个方法中有StringBuffer类型的局部变量, 其它线程不可能访问到这个局部变量, 那么会自动消除StringBuffer内的加锁操作

在Java中,监视器锁(也称为内置锁或对象锁)在实现上通常包含两种队列:

  1. 等待队列(Wait Set):等待队列用于存放那些因为调用了 Object.wait()Thread.join() 或者 LockSupport.park() 等方法而进入等待状态的线程。当线程被唤醒时,它会从等待队列中移出,重新进入锁的竞争中。
  2. 阻塞队列(Blocked Set):阻塞队列用于存放那些尝试获取锁但由于锁被其他线程持有而进入阻塞状态的线程。这些线程会被放入锁的阻塞队列中等待锁的释放。

CAS

  • CAS(Compare and Swap)是一种原子性操作, 能够在无锁的情况下实现安全的值更新操作;

  • 也可以宽泛一点就说成是自旋

  • 它的基本思想是,要修改某个值时, 先读取当前值, 记为e, 计算出要修改为的值v, 写回之前再次读取当前值n, 如果n和e不相同, 说明修改失败, 放弃修改, 重新执行此过程

  • 多核环境下, 底层是由 lock_cmpxchg 汇编指令实现的, cmpxchg这条指令并没有原子性, 必须是lock_cmpxchg指令, 前面的lock指令保证了原子性;

  • 例如由CAS + 自旋机制 实现轻量级锁机制

    1. 线程尝试获取锁时,首先会通过CAS操作尝试获取轻量级锁
    2. 如果获取锁失败, 通过自旋操作不断尝试获取锁
    while(!cas){
    count++;
    if(cnt > threshold)
    升级重量级锁;
    }
    
  • ABA问题

    当我写回之前再次读取当前值n, 即使仍然等于e, 我也不知道到底是没被人改过, 还是改过只不过最后还是改成了e;

    解决: 给值添加版本号, 读取值的时候连同版本号一起读取, 如果值相同但是版本号不同, 说明已经被修改过;

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

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

相关文章

思科OSPF动态路由配置8

#路由协议实现# #任务八OSPF动态路由配置8# 开放式最短路径优先&#xff08;Open Shortest Path First,OSPF&#xff09;协议是目前网络中应用最广泛的动态路由协议之一。它也属于内部网关路由协议&#xff0c;能够适应各种规模的网络环境&#xff0c;是典型的链路状态路由协…

ZooKeeper 集群的详细部署

ZooKeeper 集群部署 一、ZooKeeper 简介1.1 什么是 ZooKeeper1.2 ZooKeeper 特点 二 ZooKeeper 的架构和设计4.1 ZooKeeper 数据模型4.1.1 Znode 节点特性 三、ZooKeeper 的集群安装前准备工作3.1 需要的准备工作3.2 Linux 系统 3 个节点准备3.2.1 克隆3.2.2 配置另外两台服务器…

【RabbitMQ】 相关概念 + 工作模式

本文将介绍一些MQ中常见的概念&#xff0c;同时也会简单实现一下RabbitMQ的工作流程。 MQ概念 Message Queue消息队列。是用来存储消息的队列&#xff0c;多用于分布式系统之间的通信。 系统间调用通常有&#xff1a;同步通信和异步通信。MQ就是在异步通信的时候使用的。 同…

高考志愿智能推荐系统-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…

第三方软件测评中心分享:软件系统测试内容和作用

近年来&#xff0c;随着信息技术的迅猛发展&#xff0c;软件系统的应用范围不断扩大。保证软件质量的关键措施之一就是软件系统测试。软件系统测试是指在软件开发生命周期中&#xff0c;通过一系列特定的测试活动来验证和确认软件系统的性能、功能及安全性&#xff0c;确保软件…

优优嗨聚集团:餐饮合作新未来引领美食产业新风尚

在快速变化的21世纪&#xff0c;餐饮行业作为民生消费的重要组成部分&#xff0c;正经历着前所未有的变革与挑战。随着消费者需求的多元化、个性化以及科技的不断进步&#xff0c;餐饮合作的新模式正悄然兴起&#xff0c;为行业带来了前所未有的发展机遇与活力。本文将探讨餐饮…

【Redis】Redis 数据类型与结构—(二)

Redis 数据类型与结构 一、值的数据类型二、键值对数据结构三、集合数据操作效率 一、值的数据类型 Redis “快”取决于两方面&#xff0c;一方面&#xff0c;它是内存数据库&#xff0c;另一方面&#xff0c;则是高效的数据结构。 Redis 键值对中值的数据类型&#xff0c;也…

网页版IntelliJ IDEA部署

在服务器部署网页 IntelliJ IDEA 引言 大家好&#xff0c;我是小阳&#xff0c;今天要为大家带来一个黑科技——如何在云端部署和使用WEB版的IntelliJ IDEA&#xff0c;让你在任何地方都可以随心所欲地进行Java开发。这个方法特别适合那些用着老旧Windows电脑&#xff0c;部署…

MySQL集群+Keepalived实现高可用部署

Mysql高可用集群-双主双活-myqlkeeplived 一、特殊情况 常见案例&#xff1a;当生产环境中&#xff0c;当应用服务使用了mysql-1连接信息&#xff0c;在升级打包过程中或者有高频的数据持续写入【对数据一致性要求比较高的场景】&#xff0c;这种情况下&#xff0c;数据库连接…

Springboot 整合 Swagger3(springdoc-openapi)

使用springdoc-openapi这个库来生成swagger的api文档 官方Github仓库&#xff1a; https://github.com/springdoc/springdoc-openapi 官网地址&#xff1a;https://springdoc.org 目录题 1. 引入依赖2. 拦截器设置3. 访问接口页面3.1 添加配置项&#xff0c;使得访问路径变短…

贪吃蛇(C语言详解)

贪吃蛇游戏运行画面-CSDN直播 目录 贪吃蛇游戏运行画面-CSDN直播 1. 实验目标 2. Win32 API介绍 2.1 Win32 API 2.2 控制台程序&#xff08;Console&#xff09; 2.3 控制台屏幕上的坐标COORD 2.4 GetStdHandle 2.5 GetConsoleCursorlnfo 2.5.1 CONSOLE_CURSOR_INFO …

开源通用验证码识别OCR —— DdddOcr 源码赏析(一)

文章目录 [toc] 前言DdddOcr环境准备安装DdddOcr使用示例 源码分析实例化DdddOcr实例化过程 分类识别分类识别过程 未完待续 前言 DdddOcr 源码赏析 DdddOcr DdddOcr是开源的通用验证码识别OCR 官方传送门 环境准备 安装DdddOcr pip install ddddocr使用示例 示例图片如…

Wyn商业智能助力零售行业数字化决策高效驱动

最新技术资源&#xff08;建议收藏&#xff09; https://www.grapecity.com.cn/resources/ 项目背景及痛点 百利商业的业务覆盖赛格、 SKP、奥莱、王府井等多地区具有代表性的商场&#xff0c;并创立了多个自有品牌。随着新零售模式的兴起&#xff0c;百利商业紧跟时代步伐&am…

培训学校课程管理系统-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…

web开发,过滤器,前后端交互

目录 web开发概述 web开发环境搭建 Servlet概述 Servlet的作用&#xff1a; Servlet创建和使用 Servlet生命周期 http请求 过滤器 过滤器的使用场景&#xff1a; 通过Filter接口来实现&#xff1a; 前后端项目之间的交互&#xff1a; 1、同步请求 2、异步请求 优化…

Mysql(三)---增删查改(基础)

文章目录 前言1.补充1.修改表名1.2.修改列名1.3.修改列类型1.4.增加新列1.5.删除指定列 2.CRUD3.新增(Create)3.1.单行插入3.2.指定列插入3.3.多行插入 4.数据库的约束4.1.约束的分类4.2.NULL约束4.3.Unique约束4.4.Default 默认值约束4.5.PRIMARY KEY&#xff1a;主键约束4.6.…

Facebook与区块链:社交网络如何融入去中心化技术

随着区块链技术的飞速发展&#xff0c;去中心化理念逐渐渗透到各个领域&#xff0c;社交网络也不例外。作为全球领先的社交平台&#xff0c;Facebook在这一趋势下开始积极探索区块链技术的潜力&#xff0c;希望利用这一前沿技术来提升平台的安全性、透明度和用户控制权。本文将…

景联文科技:一文详解如何构建高质量SFT数据

在图像处理和计算机视觉领域中&#xff0c;将一张图像转化为可用于训练机器学习模型的数据是一项复杂而重要的任务。SFT&#xff08;Supervised Fine-Tuning&#xff0c;监督微调&#xff09;是一种常见的深度学习策略&#xff0c;在这一过程中发挥着核心作用。 SFT是指在一个预…

alibabacloud学习笔记14

阿里云ECS服务器Docker部署Nacos docker拉取镜像 查看容器&#xff1a; 启动docker&#xff1a; 查看启动日志&#xff1a; 阿里云ECSDocker部署Sentinel实战 docker拉取镜像 启动镜像&#xff1a; 查看日志&#xff1a; 记得网络安全组开放端口。 阿里云ECSDocker部署Zipkin实…

tekton什么情况下在Dockerfile中需要用copy

kaniko配置如下 如果docker中的workDir跟tekton中的workDir不一致需要copy。也可以通过mv&#xff0c;cp达到类似效果