【JUC进阶】详解synchronized锁升级

news2024/12/23 6:06:04

文章目录

  • 1. synchronized概述
  • 2. synchronized 的实现原理
    • 2.1 Java对象组成
    • 2.2 Monitor
    • 2.3 从字节码角度看synchronized
  • 3. 锁升级
    • 3.1 偏向锁
    • 3.2 轻量级锁

1. synchronized概述

synchronized是一个悲观锁,可以实现线程同步,在多线程的环境下,需在操作同步资源的时候先加锁,避免共享资源出现问题。

因为加锁可以使得一个线程在一个时间点内只有一个线程可以访问,这样增加了安全性。

但是这样却损失了程序的执行性能,因为在加锁、抢夺锁、释放锁需要从用户态切换成内核态,属于操作系统层面的,因此比较消耗性能。

于是,在JDK6之后便引入了“偏向锁”和“轻量级锁”,共有4种锁状态,级别由低到高依次为:无锁状态偏向锁状态轻量级锁状态重量级锁状态。这几个状态会随着竞争情况逐渐升级。

锁状态说明及升级图示

synchronized 可以用在实例方法、静态方法、代码块上

  1. 修饰实例方法,对当前实例对象this加锁
  2. 修饰静态方法,对当前类的Class对象加锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁

2. synchronized 的实现原理

想要了解synchronized 的实现原理,就要先知道Java对象是怎么存放的。因为synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步。


2.1 Java对象组成

Java对象分为三部分:

  1. 对象头,包括**Mark Word (标记字段)** 和 Klass Pointer(类型指针)
    • Mark Word用来存储对象自身的运行时数据
    • Klass Point 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  2. 实例变量,存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐
  3. 填充字节,由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

image-20230325143946906

Java对象有这三部分,锁就在对象的对象头Mark WordMark Word的结构如下,在64位虚拟机下,MarkWord是64bit大小的,其存储结构如下所示

img

img


2.2 Monitor

Monitor可以理解为是一个同步工具或者同步器,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

ObjectMonitor() {
    _count        = 0; //记录数
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //调用wait后,线程会被加入到_WaitSet
    _EntryList    = NULL ; //等待获取锁的线程,会被加入到该列表
}

对于一个被synchronized 修饰的方法和代码块来说

  1. 当多个线程同时访问一个方法时,这些线程会被放入EntryList队列中,此时这些线程处于阻塞(Blocked)状态。
  2. 当一个线程获取到了对象的Monitor后,就进入可运行(running)状态,执行方法,此时ObjectMonitor对象的_owner就会指向当前线程,表示当前线程获取到了锁。并且锁的计数器_count需要加一。
  3. 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的owner变为null,count减1,同时线程进入WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程进入EntryList队列,竞争到锁再进入_Owner区
  4. 当线程释放锁的时候,线程会释放Monitor对象,锁的计数器_count需要减一,当锁的计数器为0的时候,就会彻底释放锁。

Monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的。


2.3 从字节码角度看synchronized

这里有一个加锁的代码

public class Test {
    public int count = 0;
    public void addOne() {
        synchronized (this) {
            count++;
        }
    }
}

将这个Java程序编译成字节码class文件

 public void addOne();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter // 进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field count:I
        14: aload_1
        15: monitorexit // 退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit // 退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:

可见,字节码底层是通过monitorenter进入同步代码块的,通过monitorexit指令退出同步代码块的。

monitorexit指令有两个,第一个是正常退出同步代码块的情况。第二个则是由于同步代码块出现异常而出现释放锁的情况,这种设计可以有效避免死锁。


3. 锁升级

为什么会出现锁升级呢?

一开始,synchronized 无论是大并发还是小并发都属于重量级锁,效率低下,因为操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

于是,在JDK6之后,在JVM层面对synchronized 进行了优化,为了减少锁的获取和释放所带来的性能消耗,引入了“偏向锁”和“轻量级锁”。也就出现了锁升级的情况。

注意,锁只可以升级但不能降级,但是偏向锁状态可以重置为无锁状态。

img


3.1 偏向锁

偏向锁的出现,是为了应对同一个线程多次获取一个锁的情况的出现,因此没有必要每次都要竞争锁,从而降低获取锁的代价。

偏向锁的核心思想是:如果一个线程获取到了锁,那么就进入偏向模式,此时Mark Word结构也变为偏向锁模式,当这个线程再次来请求获取锁,则无需在任何同步操作,直接获取锁。

加锁的时候,如果该锁对象支持偏向锁,那么Java虚拟机会通过CAS操作,将当前线程的地址也就是线程ID记录到对象头的标记字段,并且将标记字段的最后三位设置为101。

在这里插入图片描述

如果前面通过CAS加锁、解锁的时候,对比当前线程ID和Java对象头的线程ID,如果一直,就可以直接获取锁。

如果不一致,说明存在其他线程需要竞争锁对象,那么就需要查看Java对象头的记录的线程是否存活。

如果没有存活则会将锁对象重置为无锁状态,其他线程都可以竞争将其设置为偏向锁。

如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

img


3.2 轻量级锁

轻量级锁考虑的是竞争对象的线程不多,而且线程持有锁的时间也不长的情景。

轻量级锁的获取主要有两个情况

  1. 当偏向锁关闭的时候
  2. 由于多个线程竞争导致偏向锁升级为轻量级锁

线程A在获取轻量级锁的时候,会先把锁对象的Mark Word复制一份给线程A的栈帧中创建的用于存储锁记录的空间(Displaced Mark Word),然后使用CAS把对象头的内存替换成线程A存储的锁记录。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

如果自旋次数到了线程B还没有释放锁,或者线程B还在执行,线程A还在自旋等待,这时又有一个线程C过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

轻量级锁的释放
在释放锁时,当前线程会使用CAS操作将·Displaced Mark Word内容复制回锁的MarkWord里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

img


参考:

  1. 【JUC】10. synchronized与锁升级_synchronized性能下降_起名方面没有灵感的博客-CSDN博客
  2. 详解Synchronized底层实现,锁升级的具体过程,与Lock的区别 - 掘金 (juejin.cn)
  3. synchronized四种锁状态的升级 - 掘金 (juejin.cn)
  4. 大彻大悟synchronized原理,锁的升级 - 掘金 (juejin.cn)

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

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

相关文章

信盈达CorexM4核心板STM32F407VGT6电路原理图\电源原理图\USB的工作原理\miniUSB的分类

一、电源部分电路 Micro_USB插座中的5V与GND就是直接提供VCC与GND,其中的D与D-是接在CH340芯片上的实现串口通信的数据口。 电源VCC通过一个自恢复保险丝接在一个自锁开关上,这样就分析完一个miniUSB的提供电源的原理图了。 在原理图的右上角有一个AMS1117-3.3的器件…

RabbitMQ 高级篇 | 黑马

一、消息可靠投递 生产端的 在使用 RabbitMQ的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。 RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。 confirm 确认模式return 退回模式 rabbitmq整个消息投递的路径为: producer--->ra…

HCIP-6.6BGP配置中IGP路由表和BGP路由表、IP路由表关系

BGP配置中IGP路由表和BGP路由表、IP路由表关系1、基础配置2、配置建立IBGP对等体3、配置EBGP对等体4、使用network命令宣告路由5、路由的学习5.1、R2的路由表5.2、R1的BGP路由表5.3、R3的BGP路由表5.4、R5的BGP路由表6、AS200与AS100通信6.1、AS200出接口注入AS100IP路由表6.2、…

Go 汇编详解

动手点关注干货不迷路前言我们知道 Go 语言的三位领导者中有两位来自 Plan 9 项目,这直接导致了 Go 语言的汇编采用了比较有个性的 Plan 9 风格。不过,我们不能因咽废食而放弃无所不能的汇编。1、 Go 汇编基础知识1.1、通用寄存器不同体系结构的 CPU&…

【Docker】使用Docker Compose部署项目

目录 前言 使用 前言 如果你部署项目需要很多个容器, 并且还是在手动一个一个启动的话来试试Docker Compose, 只需要写好Docker Compose文件运行命令就能帮你一次性全启动, 对微服务很友好啊~ 使用 运行以下命令查看是否有版本号, 确保你已经安装了Docker Compose docker…

使用sealos工具部署k8s

为什么使用sealos工具:简单、快、完全兼容 k8s、给100年认证 sealos使用最新版本: 官网:https://www.sealyun.com/ 码:https://github.com/labring/sealos 官方介绍什么是sealos Sealos 是以 kubernetes 为内核的云操作系统发行版…

精彩回顾|4.8 Beijing Rust Meetup

2023年4月8日,达坦科技联合南京大学、CloudWeGo、华为等技术专家成功举办了题为Rust X的meetup。开发者们不仅线下积极报名参与,更在线上直播中踊跃参与互动,一起探讨Rust作为一种强调性能、安全和并发性的编程语言的各种应用和实践。演讲者与…

从EXCEL BOM 描述中提取部分信息---正则表达式使用

从EXCEL BOM描述中提取关键的信息,用于建库填写内容,或者检查BOM等都会用到,如下大概通过两种方式实现信息的提取 1.手动Excel中提取 2.将如上1的方式用python实现,可以实现批量操作,减少操作带来的错误,…

[网络安全]第三次作业

目录 1. 什么是IDS? 2. IDS和防火墙有什么不同? 3. IDS工作原理? 4. IDS的主要检测方法有哪些详细说明? 5. IDS的部署方式有哪些? 6. IDS的签名是什么意思?签名过滤器有什么作用?例外签名…

温故c语言——深度剖析数据在内存中的存储

目录 数据类型详细介绍整形在内存中的存储:原码、反码、补码大小端字节序介绍及判断浮点型在内存中的存储解析 1. 数据类型介绍 基本内置数据类型有: //在内存中占用空间的大小 char //字符数据类型 占用1个字节 short //短整型 占用2个…

linux及openEuler破解root密码

第一步:开机的时候按键盘的字母 E 键, 进入引导模式 第二步:进入引导模式 :找到linux这一行,按键盘上的end 键,跳转到行尾,输入: init/bin/sh 修改完后,按键盘上的 ctr…

电脑开机出现英文字母开不了机U盘重装系统教学

电脑开机出现英文字母开不了机U盘重装系统教学。有用户电脑开机之后出现了错误代码字母,无法正常的开机了。遇到这个问题要怎么去进行系统的重新安装呢?一起来看看以下的具体解决方法教学吧。 准备工作: 1、U盘一个(尽量使用8G以上…

基于LS1028 TSN时间敏感网络交换机方案(一)TSN介绍

2.1 时间敏感网络介绍 时间敏感网络小组的前身是 AVB ,即以太网音视频桥接技术( Ethernet Audio/Video Bridging, 简称 Ethernet AVB ) [10] 。它在传统以太网络的基础上,通过 精确时钟同步、预留带宽、流量整形&#xff0…

Spring和IDEA都不推荐用的@Autowired注解,为什么还有那么多人用?

Autowired的默认装配 我们都知道在spring中Autowired注解,是用来自动装配对象的。通常,我们在项目中是这样用的: package com.sue.cache.service;import org.springframework.stereotype.Service;Service public class TestService1 {publ…

今天面了个京东拿 38K 出来的,让我见识到了基础的天花板

今年的春招已经开始了,很多小伙伴收获不错,拿到了心仪的 offer。 各大论坛和社区里也看见不少小伙伴慷慨地分享了常见的软件测试面试题和八股文,为此咱这里也统一做一次大整理和大归类,这也算是划重点了。 俗话说得好&#xff0…

ChatGPT接入Siri(保姆级教程)

ChatGPT接入Siri(保姆级教程)第一步:获取OpenAPI的Key第二步:制作快捷指令今天,我将为大家分享如何将GPT应用集成到苹果手机的Siri中(当然手机是需要魔法(TZ)的) 第一步…

协议篇之UART协议

协议篇之UART协议一、写在前面二、UART协议简介三、UART协议数据帧结构3.1 UART发送过程3.2 UART接收过程四、UART传输速率五、写在最后一、写在前面 由于设计需要,需要入门学习一下UART协议。本文主要学习UART协议的数据帧结构。 二、UART协议简介 通用异步收发传…

AutoGPT自主AI正在路上#趋势:自主人工智能、人机交互、终身学习

hi,大家好,我是shadow。今天分享一些我看到的人工智能正在发生的趋势,以及创新的机会。一些动态经过几个月的筹备,慢慢地我会开启2023的Mixlab故事,可查看公众号菜单栏里的2023专栏。本周六和下周六我将在上海的活动分…

Thingsboard开源物联网平台智慧农业实例快速部署教程(三)【源码部署及logo更换】

Thingsboard源码安装并更换logo 文章目录Thingsboard源码安装并更换logo1. 源码拉取2. 导入项目3. 项目编译4. 项目启动5. 修改logo6. 部署(rpm deb方式)1. 源码拉取 本教程按照3.4发行版本行进 git clone https://github.com/thingsboard/thingsboard…

深入拆解 Java 虚拟机-打卡|01 | Java代码是怎么运行的?

文章目录Java代码是怎么运行的?几个为什么为什么在虚拟机中运行?Java 虚拟机具体又是怎样运行 Java 代码的呢?Java虚拟机的运行效率怎么样?总结Java代码是怎么运行的? 来来来,运行个"Hello word !“告诉我是怎么…