Java并发之synchronized详解

☆* o(≧▽≦)o *☆嗨~我是小奥🍹
📄📄📄个人博客:小奥的博客
📄📄📄CSDN:个人CSDN
📙📙📙Github:传送门
📅📅📅面经分享(牛客主页):传送门
🍹文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
📜 如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️

文章目录

  • Java并发之synchronized详解
  • 一、synchronized
  • 二、synchronized原理
    • (1)对象头
    • (2)Monitor
    • (3)工作流程
  • 三、synchronized优化
    • (1)锁升级
      • ① 偏向锁
      • ② 轻量级锁
      • ③ 锁膨胀
      • ④ 重量级锁
    • (2) 锁优化
      • ① 自旋锁
      • ② 锁消除
      • ③ 锁粗化

Java并发之synchronized详解

一、synchronized

synchronized是Java中的关键字,是一种同步锁,他修饰的对象有以下几种:

  • 修饰一个代码块,被修饰的代码块称为同步方法块,其作用的范围是使用大括号括起来的代码,作用的对象是调用这个代码块的对象。
  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  • 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  • 修饰一个类,其作用的范围是synchronized后面括起来的部分,作用的对象是这个类的所有对象。

无论synchronized关键字加在方法上还是对象上,

  • 如果它作用的对象是非静态的,则它获取的是对象;
  • 如果它作用的对象是一个静态方法或一个类,则它获取的锁是对于类的,该类的所有对象持有同一把锁。

实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免不必要的同步控制。

二、synchronized原理

(1)对象头

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

  • 对象头:在HotSpot虚拟机中,对象头被分为两部分,分别为:Mark Word(标记字段)、Class Pointer(类型指针),如果是数组,那么还会有数组长度
  • 实例数据存储类的属性数据信息,包括父类的属性信息,这部分内存按4字节填充。
  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍,所以填充数据不是必要存在的,仅仅是为了字节对齐。

而Java对象头则是实现synchronized的锁对象的基础。

(2)Monitor

synchronized 用的锁是存在 Java 对象头里的。

  • 如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头;
  • 如果对象是非数组类型,则用 2 字宽存储对象头。

在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit.

在这里插入图片描述

Monitor的结构

Java 对象头里的 Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。另外,数组还会有长度的标识。

在运行期间,Mark Word里存储的数据会随着锁的标志位的变化而变化。

在这里插入图片描述

(3)工作流程

1、开始时Monitor中的owner为null

2、当线程1执行synchronized(obj)时就会将Monitor的所有者owner置为线程1,Monitor中只能有1个Owner,obj对象中的Mark Word指向Monitor,把对象原有的Mark Word存储线程栈中的锁记录中。

3、线程1上锁的过程中,如果有其他线程来执行synchronized(obj),就会进入EntryList BLOCKED

4、线程1执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord

5、唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞。

(4)字节码层面分析

public static void main(String[] args) {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println("ok");
    }
}
 0 new #2 <java/lang/Object>	// new Object
 3 dup
 4 invokespecial #1 <java/lang/Object.<init> : ()V>		// invokespecial <init>:()V,非虚方法
 7 astore_1						// lock引用 -> lock
 8 aload_1						// lock (synchronized开始)
 9 dup							// 一份用来初始化,一份用来引用
10 astore_2						// lock引用 -> slot 2
11 monitorenter					// 【将 lock对象 MarkWord 置为 Monitor 指针】
12 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
15 ldc #4 <ok>
17 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
20 aload_2						// slot 2(lock引用)
21 monitorexit					// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22 goto 30 (+8)
25 astore_3						// any -> slot 3
26 aload_2						// slot 2(lock引用)
27 monitorexit					// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28 aload_3
29 athrow
30 return

实现原理:同步代码块通过moniterentermoniterexit关联到一个monitor对象,进入时设置Owner为当前线程,计数+1,退出-1,除了正常入口的moniterenter,还在异常入口的地方加入了moniterexit指令。

三、synchronized优化

(1)锁升级

synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化。

在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态重量级锁状态,这几个状态会随着竞争情况逐渐升级。

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁	// 随着竞争的增加,只能锁升级,不能降级

在这里插入图片描述

① 偏向锁

解决问题:大多数情况下,**线程不仅不存在多线程竞争,而且锁总是由同一线程多次获得,在没有其他线程竞争锁时,线程每次重入锁仍然需要进行CAS操作,造成性能的损耗。**为了让多线程获得锁的代价更低而引入了偏向锁。

偏向锁加锁:

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作

  • 当一个线程访问同步块并获取锁时,会在对象头(标志位是否是01)和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
  • 如果测试成功,表示线程已经获得了锁。
  • 如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置成 1(表示当前是偏向锁):如果没有设置,则使用 CAS 竞争锁;如果设置了,则尝试使用CAS 将对象头的偏向锁指向当前线程。

偏向锁撤销:

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁

偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。

  • 它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;
  • 如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程,从安全点继续执行代码。

偏向锁关闭:

// 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0 //  关闭偏向锁的延迟
-XX:-UseBiasedLoacking=false //  关闭偏向锁

JDK8延迟4每秒开启偏向锁的原因:当程序刚开始执行时,会有很多的线程来争抢锁,如果开启偏向锁效率反而会降低。

撤销偏向锁的场景

  • 调用对象的hashCode:偏向锁的对象MarkWord中存储的是线程的id,调用hashCode导致偏向锁被撤销;
  • 当有其他线程来竞争锁的时候,会将偏向锁升级为轻量锁;
  • 调用wait/notify,需要申请Monitor,进入WaitSet。

批量撤销和批量重偏向:

从偏向锁的加锁解锁过程中就可以看出,当只有一个线程反复获取锁的是皇后,偏向锁带来的性能开销可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。

批量撤销(解决场景):在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

批量重偏向(解决场景):一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。

原理:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。

每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。
当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

② 轻量级锁

加锁:线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  • 创建锁记录对象(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word
  • 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
  • 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁
  • 如果 CAS 失败,有两种情况:
    • ​ 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数

解锁:轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

  • 当退出 synchronized 代码块(解锁时)
    • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
    • 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

③ 锁膨胀

在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
  • Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED
  • 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryListBLOCKED 线程。

在这里插入图片描述

④ 重量级锁

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

(2) 锁优化

① 自旋锁

当一个线程尝试获取锁(重量级锁)时,如果锁已经被其他线程持有,该线程会进入自旋状态,不断地重试获取锁,直到获取到为止。自旋锁避免了线程切换带来的开销,但是如果锁被持有的时间较长,自旋锁可能会导致CPU资源的浪费。

② 锁消除

锁消除是一种编译器优化技术,编译器可以在编译过程中分析代码,并根据程序的特性来消除一些不必要的锁操作

比如在单线程程序中使用锁,锁会变成多余的开销,编译器可以消除这些锁操作,从而提高程序的性能。

锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)

③ 锁粗化

锁粗化是一种优化技术,可以将多个连续的锁操作合并成一个锁操作。例如,如果程序中存在多个连续的对同一个锁的加锁和解锁操作,锁粗化可以将它们合并为一个锁操作,从而减少锁的竞争和开销,提高程序的性能。

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

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

相关文章

QtAV学习:(一)Windows下编译QtAV

QtAV 主页&#xff1a; QtAV by wang-bin 作者的编译构建说明文档&#xff1a; Build QtAV wang-bin/QtAV Wiki GitHub 我的编译环境&#xff1a; 编译环境&#xff1a;win10/msvc2015/Qt5.6.3 第一步&#xff1a;GitHub拉取代码,执行子模块初始化 地址&#xff1a; …

web前端-------弹性盒子(2)

上一讲我们谈的是盒子的容器实行&#xff0c;今天我们来聊一聊弹性盒子的项目属性&#xff1b; *******************&#xff08;1&#xff09;顺序属性 order属性&#xff0c;用于定义容器中项目的出现顺序。 顺序属性值&#xff0c;为整数&#xff0c;可以为负数&#xff…

数仓建设规范

目录 前言 一、数据模型设计规范 1.1 数仓分层原则 1.2 主题域划分原则 1.3 数据模型设计原则 1.4 数据模型管理的目标 1.5 数仓建模的方法 1.5.1 维度建模 1.5.2 三范式建模 1.5.3 三范式与维度建模区别 二、数仓公共开发规范 2.1 层次调用规范 2.2 数据类型规范…

redis(4)

文章目录 一、redis主从复制redis 主从复制架构主从复制实现命令行配置同步日志修改slave节点配置文件 主从复制故障恢复主从复制故障恢复过程介绍主从复制故障恢复实现 实现redis的级联复制主从复制优化主从复制过程主从同步优化配置 常见主从复制故障汇总master密码不对Redis…

C系列-柔性数组

&#x1f308;个人主页: 会编程的果子君 ​&#x1f4ab;个人格言:“成为自己未来的主人~” 目录 ​编辑 柔性数组 柔性数组的特点 柔性数组的使用 柔性数组的优势 柔性数组 也许你从来没有听说过柔性数组这个概念&#xff0c;但是它确实是存在的&#xff0c;C99中&#…

python 安装 流程

1. 下载python解析器。&#xff08;根据软件安装提示&#xff0c;傻瓜式操作。勾选 将其添加到path 环境变量&#xff09;Download Python | Python.org 2. 在Python环境中 安装selenium模块 命令行中 运行 pip install selenium 如果你使用的是Python3&#xff0c;可能需要…

list基本使用

list基本使用 构造迭代器容量访问修改 list容器底层是带头双向链表结构&#xff0c;可以在常数范围内在任意位置进行输入和删除&#xff0c;但不支持任意位置的随机访问&#xff08;如不支持[ ]下标访问&#xff09;&#xff0c;下面介绍list容器的基本使用接口。 template <…

租用海外服务器丢包是什么情况?

在当今的互联网时代&#xff0c;海外服务器租用已经成为了许多企业和个人的选择。然而&#xff0c;在使用海外服务器的过程中&#xff0c;有时会出现丢包的情况&#xff0c;这给用户带来了不小的困扰。那么&#xff0c;租用海外服务器丢包是什么情况呢&#xff1f;本文将对此进…

Java Arrays 的相关操作数组排序

Java Arrays 的相关操作数组排序 package com.zhong.arrays;import java.math.BigDecimal; import java.util.Arrays; import java.util.Comparator;public class ArraysDemo {public static void main(String[] args) {int[] arr {10, 20, 40, 30, 90, 60, 10, 30, 50};// A…

profinet转CANopen网关在博图如何配置profinet从站步骤

Profinet转CANopen网关&#xff08;XD-COPNm20&#xff09;是一种用于实现CANopen设备与Profinet网络连接起来进行设备之间的数据交换和通信的设备。CANopen和Profinet是两种常见的工业通信协议&#xff0c;它们在自动化控制系统中有着广泛的应用。因此CANopen转Profinet网关在…

k8s-常用工作负载控制器(更高级管理Pod)

一、工作负载控制器是什么&#xff1f; 二、Deploymennt控制器&#xff1a;介绍与部署应用 部署 三、Deployment控制器&#xff1a;滚动升级、零停机 方式一&#xff1a; 通个加入健康检查可以&#xff0c;看到&#xff0c;nginx容器逐个被替代&#xff0c;最终每个都升级完成&…

前端学习第4天

一、复合选择器 1.后代选择器 2.子代选择器 3.并集选择器 4.交集选择器 5.伪类选择器 1.伪类-超链接&#xff08;拓展&#xff09; 二、CSS特性 1.继承性 body放在style中 2.层叠性 3.优先级 属性 !important;&#xff08;最高优先级&#xff09; 1.优先级-叠加计算规则 2.em…

zabbix监控mariadb数据库

zabbix监控mariadb数据库 1.创建监控用户及授权 [rootchang ~]# mysql -uroot -p123qqq.A MariaDB [(none)]> CREATE USER monitor% IDENTIFIED BY 123qqq.A; MariaDB [(none)]> GRANT REPLICATION CLIENT,PROCESS,SHOW DATABASES,SHOW VIEW ON *.* TO monitor%; Maria…

Vue中nextTick方法的作用与原理

在Vue的开发中&#xff0c;你可能会遇到一些异步更新的问题&#xff0c;如在改变数据后需要等待DOM更新完毕后再进行下一步操作。这时就可以使用Vue提供的nextTick方法来解决这个问题。 nextTick方法的作用是在DOM更新之后执行回调函数&#xff0c;确保在下次DOM更新循环结束之…

大型软件编程实例分享,诊所门诊处方笺管理系统多台电脑同时使用的软件教程

大型软件编程实例分享&#xff0c;诊所门诊处方笺管理系统多台电脑同时使用的软件教程 一、前言 以下教程以 佳易王诊所门诊电子处方管理系统V17.2 为例说明 软件资源可以点击最下方官网卡片了解详情 软件左侧为导航栏 1、系统参数设置&#xff1a;可以设置打印等参数 2、…

为电子表格嵌入数据库,Excel/WPS一键升级为管理系统

将Excel表格转化为管理系统&#xff0c;这款工具能够实现只需导入表格数据&#xff0c;即可自动生成相应的软件和APP。 表格办公的烦恼&#xff0c;有遇到吧&#xff1f; 对于具有一定规模的企业而言&#xff0c;各类表格如同繁星般众多&#xff0c;既有日常使用的常规表格&a…

幻兽帕鲁客户端存档文件 - 云上备份和恢复教程

本文将详细介绍如何将幻兽帕鲁游戏客户端的存档文件备份至云端&#xff0c;以及如何从云端恢复存档数据至本地。 一、游戏存档备份场景 幻兽帕鲁的游戏进度存储在电脑本地磁盘上&#xff0c;游戏中创建的每个世界都对应一个本地存档文件夹。在玩游戏过程中&#xff0c;客户端…

目标检测:2如何生成自己的数据集

目录 1. 数据采集 2. 图像标注 3. 开源已标记数据集 4. 数据集划分 参考&#xff1a; 1. 数据采集 数据采集是深度学习和人工智能任务中至关重要的一步&#xff0c;它为模型提供了必要的训练样本和测试数据。在实际应用中&#xff0c;数据采集的方法多种多样&#xff0c;每…

斗破年番:七星斗宗地魔老鬼,首战吊打萧炎,毁灭莲逼出千百二老

Hello,小伙伴们&#xff0c;我是拾荒君。 国漫《斗破苍穹年番》第82期超前爆料&#xff0c;在万众瞩目之下&#xff0c;卡点帝再次展现了他的卡点救场技巧。此次&#xff0c;韩枫为了除掉萧炎&#xff0c;以他击杀魔炎谷四位长老为借口&#xff0c;请来了七品斗宗地魔老鬼。更…

MicroPython ESP32开发:通过寄存器直接访问外围设备

可以通过直接读写寄存器来控制 ESP32 的外设。这就需要阅读数据手册&#xff0c;了解要使用哪些寄存器以及要写入哪些值。下面的示例展示了如何打开和更改 MCPWM0 外设的预分频器。 from micropython import const from machine import mem32# Define the register addresses …