【多线程】synchronized使用以及原理详解

1. synchronized关键字的使用

synchronized关键字是对Java中的对象加锁,主要有3种使用形式

  1. 修饰实例(普通)方法 ,锁的是当前的实例对象;
  2. 修饰静态方法,锁的是当前类的Class对象,即使是不同的示例,他们共用的也是一把锁;
  3. 修饰代码块,所的是sybchronized括号里面的对象,这个对象可以是指定的普通实例对象,也可以是Class对象。

1.1. 修饰实例方法

修饰实例(普通)方法 ,锁的是当前的实例对象.

public class SyncExample {// synchronized 修饰实例方法,锁的是当前对象 thispublic synchronized void instanceMethod() {System.out.println(Thread.currentThread().getName() + " acquired lock on instance method");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on instance method");}public static void main(String[] args) {SyncExample example1 = new SyncExample();SyncExample example2 = new SyncExample();// 线程测试实例方法锁new Thread(example1::instanceMethod, "Thread-1").start();new Thread(example2::instanceMethod, "Thread-2").start(); // 不同对象,互不影响// 执行结果:
//         Thread-1 acquired lock on instance method
//         Thread-2 acquired lock on instance method
// 			Thread-1 released lock on instance method
// 			Thread-2 released lock on instance method}
}

执行结果:

Thread-1 acquired lock on instance method

Thread-2 acquired lock on instance method

Thread-1 released lock on instance method

Thread-2 released lock on instance method

再看一个例子:

public class SyncExample {// synchronized 修饰实例方法,锁的是当前对象 public synchronized void instanceMethod() {System.out.println(Thread.currentThread().getName() + " acquired lock on instance method");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on instance method");}public static void main(String[] args) {SyncExample example1 = new SyncExample();// 线程测试实例方法锁new Thread(example1::instanceMethod, "Thread-1").start();new Thread(example1::instanceMethod, "Thread-2").start(); // 同一个对象,阻塞// 执行结果:
// Thread-1 acquired lock on instance method
// Thread-1 released lock on instance method
// Thread-2 acquired lock on instance method
// Thread-2 released lock on instance method}
}

// 执行结果:

// Thread-1 acquired lock on instance method

// Thread-1 released lock on instance method

// Thread-2 acquired lock on instance method

// Thread-2 released lock on instance method

1.2. 修饰静态方法

由于staticMethod是static的,并且被synchronized修饰,所以它锁定的是SyncExample类的Class对象,这意味着所有SyncExample类的实例共享同一个锁。

public class SyncExample {// synchronized 修饰实例方法,锁的是Class对象 public static synchronized void staticMethod() {System.out.println(Thread.currentThread().getName() + " acquired lock on instance method");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on instance method");}public static void main(String[] args) {SyncExample example1 = new SyncExample();SyncExample example2 = new SyncExample();// 线程测试实例方法锁// 线程1测试实例方法锁Thread thread1 = new Thread(() -> {staticMethod();});// 线程2测试实例方法锁Thread thread2 = new Thread(() -> {staticMethod();});thread1.start();thread2.start();}
}

执行结果

1.3. 修饰代码块

在这个代码中,我们创建了两个线程thread1thread2,它们分别调用example1example2blockMethod方法。由于每个SyncExample实例都有自己的blockLock对象,所以这两个线程可以同时执行各自的blockMethod方法,不会互相阻塞。这展示了同步代码块可以根据需要锁定不同的对象,从而实现更细粒度的控制。

public class SyncExample {private final Object blockLock = new Object();// synchronized 修饰代码块,锁的是指定的对象public void blockMethod() {synchronized (blockLock) { // 锁住 blockLock 对象System.out.println(Thread.currentThread().getName() + " acquired lock on block");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on block");}}public static void main(String[] args) {SyncExample example1 = new SyncExample();SyncExample example2 = new SyncExample();// 线程1测试同步代码块锁Thread thread1 = new Thread(() -> {example1.blockMethod();});// 线程2测试同步代码块锁Thread thread2 = new Thread(() -> {example2.blockMethod();});thread1.start();thread2.start();}
}

如果是下面的代码呢?

public class SyncExample {private final Object blockLock = new Object();// synchronized 修饰代码块,锁的是指定的对象public void blockMethod() {synchronized (blockLock) { // 锁住 blockLock 对象System.out.println(Thread.currentThread().getName() + " acquired lock on block");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on block");}}public static void main(String[] args) {SyncExample example1 = new SyncExample();// 线程1测试同步代码块锁Thread thread1 = new Thread(() -> {example1.blockMethod();});// 线程2测试同步代码块锁Thread thread2 = new Thread(() -> {example1.blockMethod();});thread1.start();thread2.start();}
}

2. synchronized的锁原理

2.1. 同步代码块

synchronized 代码块是由一对 monitorentermonitorexit 指令实现的,Monitor 对象是同步的基本实现单元

public void foo(Object lock) {synchronized (lock) {lock.hashCode();}}// 上面的 Java 代码将编译为下面的字节码public void foo(java.lang.Object);Code:0: aload_11: dup2: astore_23: // 上面的 Java 代码将编译为下面的字节码4: aload_15: invokevirtual java/lang/Object.hashCode:()I8: pop9: aload_210: monitorexit11: goto          1914: astore_315: aload_216: monitorexit17: aload_318: athrow19: returnException table:from    to  target type4    11    14   any14    17    14   any

synchronized 在修饰同步代码块时,是由 monitorentermonitorexit 指令来实现同步的。进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。

2.2. 同步方法

synchronized 修饰同步方法时,会设置一个 ACC_SYNCHRONIZED 标志。当方法调用时,调用指令将会检查该方法是否被设置 ACC_SYNCHRONIZED 访问标志。如果设置了该标志,执行线程将先持有 Monitor 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 Mointor 对象,当方法执行完成后,再释放该 Monitor 对象。 究其根源使用的还是monitorentermonitorexit

public static synchronized void target(){}

上面的 Java 代码将编译为下面的字节码

3. synchronized可重入原理

synchronized 可重入的原理是基于 Java 中的每个对象都有一个锁(或者说是监视器锁,Monitor),以及每个线程都有一个锁计数器。

  1. 锁对象:当一个线程执行到一个 synchronized 同步代码块或者同步方法时,它需要先获取到该代码块或方法对应的对象锁。如果该锁已经被其他线程占用,则当前线程会阻塞,直到该锁被释放。
  2. 线程的锁计数器:每个线程都有一个锁计数器,用于记录该线程持有的锁的数量。当线程首次进入一个 synchronized 同步代码块时,它会尝试获取锁,如果成功,锁计数器会递增。
  3. 可重入性:如果同一个线程再次尝试获取同一个对象的锁(即再次进入 synchronized 同步代码块或者调用 synchronized 同步方法),因为该线程已经持有该对象的锁,所以它可以再次成功获取锁,并且锁计数器再次递增。这就是 synchronized 的可重入性。
  4. 释放锁:当线程执行完 synchronized 同步代码块或方法后,它会释放锁,锁计数器会递减。只有当锁计数器减到0时,锁才会被真正释放,其他线程才有机会获取该锁。
  5. 避免死锁synchronized 的可重入性避免了死锁的发生。如果一个 synchronized 方法在其执行过程中调用了另一个 synchronized 方法,并且这两个方法都需要同一个对象的锁,那么可重入性确保了同一个线程可以再次获取到锁,从而避免了死锁。
  6. 锁的释放:当线程执行完所有的 synchronized 同步代码块或方法后,锁计数器会减到0,此时锁会被释放,其他线程可以获取该锁。

4. synchronized锁升级

4.1. Java对象头

在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。 Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示:

  • 锁状态:2位,用于标识锁的状态。
  • 是否偏向锁:1位,表示对象是否启用偏向锁。
  • 分代年龄:4位,用于记录对象的分代年龄,用于垃圾收集。
  • 线程ID:偏向锁时记录持有偏向锁的线程ID。
  • Epoch:偏向锁时记录一个时间戳,用于记录偏向锁的有效期。
  • hashCode:无锁时存储对象的哈希码。
  • 其他:根据锁状态的不同,Mark Word 中还可能包含其他信息,如轻量级锁时指向栈中锁记录的指针,重量级锁时指向互斥量(重量级锁)的指针等

锁升级功能主要依赖于 Mark Word 中的锁标志位和是否偏向锁标志位,synchronized 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。 Java 1.6 引入了偏向锁和轻量级锁,从而让 synchronized 拥有了四个状态:

  • 无锁状态(unlocked)
  • 偏向锁状态(biasble)
  • 轻量级锁状态(lightweight locked)
  • 重量级锁状态(inflated)

4.2. 锁升级过程

锁升级是 JVM 为了提高锁的性能而采取的一种优化策略,主要经历了以下几个阶段:

  1. 偏向锁:当一个线程尝试获取锁时,如果该锁是偏向锁,并且当前线程是第一次获取该锁,JVM 会在 Mark Word 中记录该线程的ID,并标记为偏向锁状态。这样,当该线程再次获取锁时,就无需进行任何同步操作,直接获取锁
  2. 轻量级锁:如果另一个线程尝试获取同一个偏向锁,JVM 会先撤销偏向锁,然后将锁升级为轻量级锁。轻量级锁使用 CAS 操作尝试获取锁,如果成功,则在栈中创建一个锁记录,用于存储 Mark Word 的备份,并且将对象本来的MarkWord种存储的信息换位指向自己的LR(LOCK RECORD)指针。
  3. 重量级锁:如果轻量级锁竞争失败,即多个线程同时通过 CAS 尝试获取锁,JVM 会将轻量级锁升级为重量级锁。此时,Mark Word 会指向一个 Monitor 对象,线程获取锁失败后会被放入 Monitor 的 EntryList 集合中等待。

5. synchronized的锁优化

除了锁升级优化,Java 还使用了编译器对锁进行优化。 以下是一些主要的编译器锁优化技术:

  1. 锁消除(Lock Elision)

锁消除是 JVM 提供的一种高级优化技术,它允许编译器在确定锁对象不会引起线程安全问题时,减少不必要的加锁操作,从而提升程序性能。这种优化对开发者是透明的,由 JVM 自动进行。例如,如果一个方法内部创建了一个局部变量,并且该变量仅在当前方法栈帧中存在,不会被其他线程并发访问,JVM 就可以进行锁消除的优化

  1. 自适应锁(Adaptive Locking)

自适应锁优化是指 JVM 根据过去锁获取的行为自适应地选择是使用自旋锁还是挂起线程。如果一个锁通常被持有的时间很短,JVM 可能会选择自旋;如果一个锁通常被持有的时间很长,JVM 则可能会选择挂起线程。这种优化可以在运行时根据锁的使用模式动态调整,以实现更好的性能

  1. 锁粗化(Lock Coarsening)

如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。

锁粗化是将多个相邻的同步块合并成一个较大的同步块的过程,这些同步块使用相同的锁对象。如果编译器不能通过锁消除来减少锁的开销,它可能会通过锁粗化来减少开销。这种方法可以减少锁的获取和释放次数,从而减少指令数量和内存总线上的同步流量,但可能会增加锁的持有时间,从而增加其他线程被阻塞的时间


参考:

1. Java并发核心机制 | JAVACORE

2. https://zhuanlan.zhihu.com/p/82857579

3. Java对象结构详解【MarkWord 与锁的实现原理】 - Java程序员进阶 - 博客园

4. https://zhuanlan.zhihu.com/p/290991898

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

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

相关文章

项目开发实践——基于SpringBoot+Vue3实现的在线考试系统(二)

文章目录 一、登录功能实现1、前端实现1.1 创建登录组件1.2 安装和配置Element Plus1.3 安装axios和调用后端接口2、后端实现2.1 创建数据表和准备数据2.2 配置MYSQL配置信息2.3 登录功能实现2.3.1 创建实体类2.3.2 创建登录服务接口及实现2.3.3 创建Mapper2.3.4 实现登录接口A…

ESP32-S3-devKitC-1 点亮板上的WS2812 RGB LED

ESP32-S3-devKitC-1 板上自带了一个RGB LED,型号为 WS2812。 RGB LED 在板上的位置如下图所示。 为了点亮这个WS2812,需要确定这颗RGB LED连接到哪个GPIO上了。 下面是确定GPIO管脚的过程: 1、根据原理图 2、根据PCB布局图: 程…

汽车电子元件的可靠性保障:AEC-Q102认证

AEC-Q102标准的起源与价值 随着汽车电子系统的日益复杂,电子器件必须能够在极端的温度、湿度、振动和电磁干扰等恶劣条件下保持性能。AEC-Q102标准由汽车电子委员会(AEC)制定,专门针对LED、激光二极管和光电二极管等光电器件&…

SQL中的联结表

本文介绍什么是联结,为什么使用联结,以及如何编写使用联结的SELECT语句。 1. 联结 SQL最强大的功能之一就是能在数据查询的执行中联结(join)表。联结是SQL的SELECT能执行的最重要的操作,理解联结及其语法是学习SQL的…

每天40分玩转Django:实操 Todo List应用

实操 Todo List应用 一、今日学习内容概述 学习模块重要程度预计学时主要内容项目初始化⭐⭐⭐⭐0.5小时创建项目、配置环境模型设计⭐⭐⭐⭐⭐1小时Todo模型设计与实现CRUD视图⭐⭐⭐⭐⭐2小时实现增删改查功能模板开发⭐⭐⭐⭐1.5小时页面布局与交互设计功能测试⭐⭐⭐1小时…

Freertos任务切换

一、操作系统进行任务切换的时机: 采用信号量实现任务的互斥: 二、FreeRTOS 任务切换场合 PendSV 中断的时候提到了上下文(任务)切换被触发的场合: ● 可以执行一个系统调用 ● 系统滴答定时器(SysTick)中断。 1、执行系统调用 执行系统…

【Linux】自定义项目-进度条

更多精彩内容..... 🎉❤️播主の主页✨😘 Stark、-CSDN博客 准备工作:"\r"与"\n"字符 ①:基本含义 在C语言和Linux环境中,\r是回车符,\n是换行符,用于控制文本格式和输出…

【ubuntu24.04】PDFMathTranslate 本地PDF翻译GPU部署

https://huggingface.co/spaces/reycn/PDFMathTranslate-Docker排不上号官方都是要安装包,感觉可以本地试着源码部署一下, http://localhost:7860/官方是这个端口,但是我本地启动是:5000IDEA 里本地 backend启动效果 GUI 是监听7860的

基于卷积神经网络的垃圾分类系统实现(GUI应用)

1.摘要 本文主要实现了一个卷积神经网络模型进行垃圾图像分类,为了提高垃圾分类模型的准确率,使用使用Batch Normalization层、使用早期停止策略来防止过拟合等方法来优化模型,实验结果显示最终优化后的模型准确率较高90%左右。最终&#xf…

Vulnstack红日安全内网域渗透靶场2实战攻略

一:环境搭建 新增的网卡VMnet2,ip调成10段。 PC配置如下: DC在该环境中充当是域控。DC配置如下 : WEB配置:需要两块网卡,相当于网关服务器。 作者把外网网段都写成了192.168.111.1/24,我们可以…

Http协议在网站中的体现

文章目录 1. Http协议简介2. 网站中的体现2.1 访问网站2.2 请求2.3 请求头2.4 请求方式2.5 响应 3. 总结 1. Http协议简介 HTTP(超文本传输协议) 是一种广泛应用于互联网上的应用层协议,用于在Web浏览器和Web服务器之间传输数据。HTTP协议定…

JavaWeb——前端三剑客

前言:今天开始学习JavaWeb相关内容了,陆续会总结一些前后端分离开发的相关知识,有遗漏和错误的地方欢迎大家指出~ 目录 一、前端三剑客二、HTML2.1 HTML介绍2.2 常用标签 三、 CSS3.1 CSS介绍3.2 CSS引用的三种形式3.3. CSS选择器及其类型3.…

【经典】制造供应链四类策略(MTS、MTO、ATO、ETO)细说

关注作者 制造供应链的牛鞭问题与复杂问题主要是从两个方面解决,一是同步化供应链消减从需求到供应的放大效应,二是供应链细分,针对不同的客户、不同的需求供应的匹配策略来应对复杂性,更好的满足客户并以最低的总成本来实现。 对…

fixture装饰器

普通代码案例: python本身执行 import pytestdef init_new():print("init_new...")return Truedef test_case(init_new):if init_new is True:print("如果init_new返回True,就执行用例test_case")if __name__ __main__:#用python本…

数智读书笔记系列008 智人之上:从石器时代到AI时代的信息网络简史

书名:智人之上:从石器时代到AI时代的信息网络简史 作者:[以]尤瓦尔赫拉利 译者:林俊宏 出版时间:2024-09-01 ISBN:9787521768527 中信出版集团制作发行 作者信息 尤瓦尔・赫拉利 1976 年出生于以色列海法,是牛津大学历史学…

【C++】10___文件操作

目录 一、文本文件---写文件 二、文本文件---读文件 三、二进制文件---写文件 四、二进制文件---读文件 一、文本文件---写文件 通过文件操作可以将数据持久化 对文件操作需要包含头文件<fstream> 文件类型分两类&#xff1a; 文本文件&#xff1a;文件以文本的ASCII码形…

信号处理:概念、技术、领域

目录 基本概念 主要技术 应用领域 信号处理是一个涉及分析、修改和再生信号的多学科领域。信号可以是各种形式的&#xff0c;例如声音、图像、视频或其他类型的监测数据。信号处理的主要目标是提取有用的信息并增强信号的质量。以下是信号处理的一些基本概念和应用&#xff…

排序算法(5):归并排序

问题 排序 [30, 24, 5, 58, 18, 36, 12, 42, 39] 归并排序 归并排序采用分治法&#xff0c;将序列分成若干子序列&#xff0c;每个子序列有序后再合并成有序的完整序列。 在数组排序中&#xff0c;如果只有一个数&#xff0c;那么它本身就是有序的。如果有两个数&#xff0…

vue 自定义组件image 和 input

本章主要是介绍自定义的组件&#xff1a;WInput&#xff1a;这是一个验证码输入框&#xff0c;自动校验&#xff0c;输入完成回调等&#xff1b;WImage&#xff1a;这是一个图片展示组件&#xff0c;集成了缩放&#xff0c;移动等操作。 目录 一、安装 二、引入组件 三、使用…

基于Java的图书借阅管理系统详细设计和实现

目录 一、前言介绍&#xff1a; 二、主要技术&#xff1a; 2.1 Java技术介绍&#xff1a; 2.2 MYSQL数据库&#xff1a; 三、系统设计&#xff1a; 3.1 系统架构设计&#xff1a; 3.2 登录时序图设计&#xff1a; 四、功能截图&#xff1a; 4.1 用户登录注册 4.2 系统…