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 Object3 dup4 invokespecial #1 <java/lang/Object.<init> : ()V>		// invokespecial <init>:()V,非虚方法7 astore_1						// lock引用 -> lock8 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 逃逸分析)

③ 锁粗化

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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ensp实验合集(二)

实验6 VLAN划分....................................................................... - 30 - 实验7 路由器调试及常用命令使用........................................ - 42 - 实验8 配置静态路由器............................................................…

R语言学习case10:ggplot基础画图Parallel Coordinate Plot 平行坐标图

step1: 导入ggplot2库文件 library(ggplot2)step2&#xff1a;带入自带的iris数据集 iris <- datasets::irisstep3&#xff1a;查看数据信息 dim(iris)维度为 [150,5] head(iris)查看数据前6行的信息 step4&#xff1a;利用ggplot工具包绘图 plot5 <- ggparcoord(…

如何从 iPhone 上恢复永久删除的照片

您的 iPhone 上缺少照片吗&#xff1f;讽刺的是&#xff0c;iPhone 的许多高级功能可能正是这个问题如此普遍的原因。幸运的是&#xff0c;还有很多方法可以从 iPhone 恢复已删除的照片&#xff0c;具体取决于您设备的设置方式。 本文涵盖了所有这些内容。该过程根据您的具体情…

Oracle12c之Sqlplus命令行窗口基本使用

Oracle12c之Sqlplus命令行窗口基本使用 文章目录 Oracle12c之Sqlplus命令行窗口基本使用1. 连接1. 超级用户2. 普通用户1. 创建普通用2. 连接 2. 修改用户连接数1. 查看默认连接最多用户数1. PL/SQL developer中查看2. Sqlplus中查看 2. 查看目前已经连接的用户数3. 修改用户连…

unordered_map和unordered_set

目录 一、unordered_map 1.1、unordered_map的特点 1.2、unordered_map和map的区别 二、unordered_set 2.1、unordered_set的特点 2.2、unordered_set和set的区别 三、哈系桶的改造 3.1 结构设置 3.2 构造函数和析构函数 3.3 数据插入 3.4 数据查找 3.5 数据删除 …

Elasticsearch(ES) 简述请求操作索引下文档 增删查改操作

上文 Elasticsearch(ES) 创建带有分词器规则的索引 带着大家创建了一个带有分词功能的索引 老规矩 我们启动一下ES服务 本文 我们就来说说 关于文档的操作 我们先来添加一个文档 就像数据库加一条数据一样 这里 并不需要指定什么表结构和数据结构 它的文档结构是无模式的 添…

【代码随想录-哈希表】两个数组的交集

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

什么是功能安全?

前言 在上一家公司的时候&#xff0c;有幸参加过公司内部的技术分享会&#xff0c;有一个同事跟我们分享了功能安全的一些内容。在提问环节&#xff0c;我问了一个问题“什么是功能安全&#xff1f;”他回答不上来。这也是我们很多人在工作中常犯的一个问题&#xff1a;我们做了…

天拓四方:边缘计算网关功能、特点与应用举例

传统的数据处理方式面临网络延迟、带宽限制和安全风险等问题。为了解决这些问题&#xff0c;边缘计算技术应运而生&#xff0c;而边缘计算网关作为其核心组件&#xff0c;正发挥着越来越重要的作用。边缘计算网关位于数据源和云数据中心之间。它具备数据采集、协议转换、数据处…

NLP_语言模型的雏形N-Gram

文章目录 N-Gram 模型1.将给定的文本分割成连续的N个词的组合(N-Gram)2.统计每个N-Gram在文本中出现的次数&#xff0c;也就是词频3.为了得到一个词在给定上下文中出现的概率&#xff0c;我们可以利用条件概率公式计算。具体来讲&#xff0c;就是计算给定前N-1个词时&#xff0…

打开双重el-dialog后出现遮罩后如何解决?

背景&#xff1a; 打开el-dialog后&#xff0c;再次打开另外一个el-dialog&#xff0c;出现以下画面。 解决方式&#xff1a;在第二个el-dialog增加append-to-body <el-dialog :close-on-click-modal“true” :visible.sync“createVisible” v-if“createVisible” :width…

ReactNative实现弧形拖动条

我们直接看效果 先看下面的使用代码 <CircularSlider5step{2}min{0}max{100}radius{100}value{30}onComplete{(changeValue: number) > this.handleEmailSbp(changeValue)}onChange{(changeValue: number) > this.handleEmailDpd(changeValue)}contentContainerStyle{…