深入理解 Java 中的 synchronized 代码块

目录

前言

一、synchronized的工作原理

二、使用synchronized代码块的场景

三、编写synchronized代码块的最佳实践

四、何时使用 synchronized 代码块?

同步:

不同步:

五、Demo讲解

1.使用synchronized代码块减小锁的粒度,提高性能

2.synchronized可以使用任意的object进行加锁

 3.不要使用String的常量加锁,会出现死循环问题

4.锁对象的改变问题

5.死锁问题  


前言

        在多线程编程中,确保共享资源的安全访问是一个关键问题。Java 提供了多种机制来实现线程同步,其中 synchronized 关键字是最常用的一种。在本文中,我们将深入探讨 synchronized 代码块的使用和原理,并通过示例展示其应用。

一、synchronized的工作原理

synchronized关键字可以应用于方法或代码块,以确保同一时间只有一个线程可以执行被synchronized修饰的代码。其工作原理主要基于Java对象头中的Monitor(监视器)。每个Java对象都有一个与之关联的Monitor,线程在访问synchronized代码块时,需要首先获得该对象的Monitor的所有权。

  • synchronized方法:当一个线程进入一个对象的synchronized(this)方法时,它自动获取该对象的Monitor的所有权,并在方法返回或抛出异常时释放Monitor。
  • synchronized代码块synchronized代码块允许我们更精细地控制哪些代码需要被同步。线程在访问synchronized(object)代码块时,需要获得指定对象object的Monitor的所有权。

二、使用synchronized代码块的场景

  • 保护共享资源:当多个线程需要访问和修改同一份数据时,可以使用synchronized代码块来确保同一时间只有一个线程能够访问这些数据。
  • 避免死锁:与synchronized方法相比,synchronized代码块提供了更细粒度的同步控制,有助于减少死锁的风险。通过只同步必要的代码段,可以减少线程之间的竞争和等待时间。
  • 提高性能:在某些情况下,使用synchronized代码块可以避免不必要的同步开销。例如,当一个方法中的大部分代码都不需要同步时,可以将需要同步的代码段封装在synchronized代码块中。

三、编写synchronized代码块的最佳实践

  • 尽量减小同步范围:只将需要同步的代码段放在synchronized代码块中,避免不必要的同步开销。
  • 避免在同步块中调用可能阻塞的方法:在同步块中调用可能阻塞的方法(如IO操作、等待用户输入等)会导致其他等待Monitor的线程也被阻塞,从而降低系统的并发性能。
  • 注意锁的粒度:过细的锁粒度可能导致线程之间的竞争加剧,而过粗的锁粒度则可能导致不必要的同步开销。因此,在选择锁的粒度时需要权衡这两个因素。
  • 避免嵌套锁:尽量避免在一个已经持有某个对象Monitor的线程中再次请求该对象或其他对象的Monitor。这可能导致死锁或其他并发问题。
  • 考虑使用ReentrantLock等高级并发工具:虽然synchronized关键字简单易用,但在某些复杂场景下可能需要更灵活的同步控制。此时可以考虑使用Java并发包中的ReentrantLock、Semaphore等高级并发工具。

四、何时使用 synchronized 代码块?

同步:

  1. 访问共享资源:当多个线程需要访问同一个对象或变量时,应该使用同步。
  2. 修改共享状态:当多个线程修改同一个对象的状态时,应该使用同步。
  3. 执行原子操作:当需要保证某些操作的原子性(即操作不可分割)时,应该使用同步。

不同步:

  1. 只读操作:如果多个线程只是读取共享资源,而不修改它,不需要同步。
  2. 局部变量:局部变量是线程私有的,不需要同步。
  3. 无共享资源:如果多个线程操作的资源相互独立,不需要同步。

五、Demo讲解

1.使用synchronized代码块减小锁的粒度,提高性能

使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步的方法执行一个很长时间的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况下可以使用synchronized代码块去优化代码执行时间,也就是通常所说的减小锁的粒度。

package com.ctb.sync6;/*** 使用synchronized代码块减小锁的粒度,提高性能* * @author biao** 2024年*/
public class Optimize {public void doLongTimeTask(){try {System.out.println("当前线程开始:" + Thread.currentThread().getName() + ", 正在执行一个较长时间的业务操作,其内容不需要同步");Thread.sleep(2000);synchronized(this){System.out.println("当前线程:" + Thread.currentThread().getName() + ", 执行同步代码块,对其同步变量进行操作");Thread.sleep(1000);}System.out.println("当前线程结束:" + Thread.currentThread().getName() +", 执行完毕");} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {final Optimize otz = new Optimize();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {otz.doLongTimeTask();}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {otz.doLongTimeTask();}},"t2");t1.start();t2.start();}}

结果: 

:使用 synchronized 代码块减小锁的粒度,以提高多线程程序的性能。在 doLongTimeTask() 方法中,通过将长时间业务操作与需要同步的操作分别放置在不同的代码块中,两个线程可以更有效地并发执行需要同步的操作,可以减小锁的粒度,使得只有在必要的部分才会被同步,从而提高了并发执行的效率。

这样做的好处是在确保线程安全的同时,尽量减少同步的范围,避免不必要的阻塞,从而提升程序的性能。也是多线程编程中常用的优化手段之一

2.synchronized可以使用任意的object进行加锁

package com.ctb.sync6;/*** 使用synchronized代码块加锁,比较灵活* * @author biao** 2024年*/
public class ObjectLock {public void method1(){synchronized (this) {	//对象锁try {System.out.println("do method1..");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}public void method2(){		//类锁synchronized (ObjectLock.class) {try {System.out.println("do method2..");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}private Object lock = new Object();public void method3(){		//任何对象锁synchronized (lock) {try {System.out.println("do method3..");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {final ObjectLock objLock = new ObjectLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {objLock.method1();}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {objLock.method2();}});Thread t3 = new Thread(new Runnable() {@Overridepublic void run() {objLock.method3();}});t1.start();t2.start();t3.start();}}

结果: 

 注:在多线程环境下使用 synchronized 代码块来实现不同类型锁的加锁操作,通过展示对象锁、类锁和任意对象锁的使用方式,说明了不同加锁方式在多线程并发环境中的作用,用法比较灵活。

 3.不要使用String的常量加锁,会出现死循环问题

package com.ctb.sync6;
/*** synchronized代码块对字符串的锁,注意String常量池的缓存功能* * @author biao** 2024年*/
public class StringLock {public void method() {//new String("字符串常量")synchronized ("字符串常量") {try {while(true){System.out.println("当前线程 : "  + Thread.currentThread().getName() + "开始");Thread.sleep(1000);		System.out.println("当前线程 : "  + Thread.currentThread().getName() + "结束");}} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {final StringLock stringLock = new StringLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {stringLock.method();}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {stringLock.method();}},"t2");t1.start();t2.start();}
}

 结果:

注:StringLock 类中,定义了一个 method() 方法,该方法使用 "字符串常量" 作为锁对象来实现同步操作。在方法中,通过 synchronized("字符串常量") 来对代码块进行加锁,确保多个线程在执行该代码块时是互斥的。

main 方法中,创建了两个线程 t1 和 t2 分别执行 method() 方法。由于两个线程共享同一个字符串常量作为锁对象,因此它们在执行 method() 方法时会相互竞争这个锁。  

public void method() {//new String("字符串常量")synchronized (new String("字符串常量")) {try {while(true){System.out.println("当前线程 : "  + Thread.currentThread().getName() + "开始");Thread.sleep(1000);		System.out.println("当前线程 : "  + Thread.currentThread().getName() + "结束");}} catch (InterruptedException e) {e.printStackTrace();}}}

 结果:

注:“字符串常量”它是只有一个引用,尽量不要拿字符串常量这种方式去加锁,我们可以使用new String("字符串常量"),注意即可  

4.锁对象的改变问题

当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的锁就不同,如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了改变。  

package com.ctb.sync6;
/*** 锁对象的改变问题* * @author biao** 2024年*/
public class ChangeLock {private String lock = "lock";private void method(){synchronized (lock) {try {System.out.println("当前线程 : "  + Thread.currentThread().getName() + "开始");lock = "change lock";Thread.sleep(2000);System.out.println("当前线程 : "  + Thread.currentThread().getName() + "结束");} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {final ChangeLock changeLock = new ChangeLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {changeLock.method();}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {changeLock.method();}},"t2");t1.start();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}

结果:

注:当我们使用字符串常量作为一把锁的时候,一定要注意在synchronized代码块里尽量不要去修改该锁对象的内容,第一个线程拿到的锁lock的值是lock,第二个线程拿到的是change lock去获得锁  

private void method(){synchronized (lock) {try {System.out.println("当前线程 : "  + Thread.currentThread().getName() + "开始");//lock = "change lock";Thread.sleep(2000);System.out.println("当前线程 : "  + Thread.currentThread().getName() + "结束");} catch (InterruptedException e) {e.printStackTrace();}}}

结果:

注://lock = "change lock";当我们不去修改该锁对象的内容时,他将会依次去获取锁。  

package com.ctb.sync6;
/*** 同一对象属性的修改不会影响锁的情况* * @author biao** 2024年*/
public class ModifyLock {private String name ;private int age ;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public synchronized void changeAttributte(String name, int age) {try {System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 开始");this.setName(name);this.setAge(age);System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 修改对象内容为: " + this.getName() + ", " + this.getAge());Thread.sleep(2000);System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 结束");} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {final ModifyLock modifyLock = new ModifyLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {modifyLock.changeAttributte("张三", 20);}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {modifyLock.changeAttributte("李四", 21);}},"t2");t1.start();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}

 结果:

注:由于 changeAttribute 方法使用了对象锁,因此在多线程环境下,只有一个线程能够执行该方法,保证了对同一个对象的属性修改操作是互斥的。一个对象里面的属性发生改变的时候,是不影响锁的变化的,还是这个对象。

使用对象锁实现了对同一对象属性的修改操作的线程安全性,确保了多线程环境下对对象属性的修改操作是同步的,避免了数据不一致的情况发生  

5.死锁问题  

package com.ctb.sync6;/*** 死锁问题,在设计程序时就应该避免双方相互持有对方的锁的情况* * @author biao** 2024年*/
public class DeadLock implements Runnable{private String tag;private static Object lock1 = new Object();private static Object lock2 = new Object();public void setTag(String tag){this.tag = tag;}@Overridepublic void run() {if(tag.equals("a")){synchronized (lock1) {try {System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock1执行");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2) {System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock2执行");}}}if(tag.equals("b")){synchronized (lock2) {try {System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入了lock2执行");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1) {System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入了lock1执行");}}}}public static void main(String[] args) {DeadLock d1 = new DeadLock();d1.setTag("a");DeadLock d2 = new DeadLock();d2.setTag("b");Thread t1 = new Thread(d1, "t1");Thread t2 = new Thread(d2, "t2");t1.start();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}

 结果:

注:run() 方法中,如果 tag 的取值为 "a",则线程会先获取 lock1,然后尝试获取 lock2;如果 tag 的取值为 "b",则线程会先获取 lock2,然后尝试获取 lock1

当两个线程同时运行时,它们会陷入死锁状态,因为彼此持有对方需要的锁而无法释放,导致程序无法继续执行下去,最终需要手动终止程序。

为避免死锁,应该设计程序避免多个线程竞争多个锁的情况,或者确保多个锁的获取顺序是一致的,从而避免循环等待的情况发生。  

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

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

相关文章

勒索病毒剖析

2016年不自己勒索了 卖病毒 让别人勒索 傻瓜式勒索 黑客用的是非对称加密 全世界只有黑客有那把私钥 反向解密不了 传统爆破容易被检测,黑客慢速爆破,利用超级多的僵尸进行试错,慢慢试出来账号密码 因为一般运维设备在防火墙的白名单里&…

增值税发票OCR识别API在Java、Python、PHP中的使用教程

​增值税发票OCR识别是一种利用光学字符识别(OCR)技术,自动提取和识别增值税发票上的文字信息的过程。增值税发票是企业在销售商品或提供服务过程中产生的一种税务凭证,包含了关键的财务信息,如商品明细、金额、税率等…

【Spine学习09】之导入皮肤两种方式[skin]

第一种:明确项目中某个角色是有两套皮肤的情况下 直接导入两套皮肤 1、添加SKIN皮肤指令 2、在ps中-文件-脚本-浏览【打开选中jsx脚本】 3、打开Spine 点击左上角,选择导入数据 就可以看到该角色的两套皮肤啦! 第二种:刚开始角…

[DDR4] 总目录 学习路线

依公知及经验整理,原创保护,禁止转载。 传送门: 总目录 目录 基础篇 1-1 DDR4 发展历史 1-2 DDR4 和 DDR3 差异与优势 1-3 DDR4 内部结构 1-4 DDR4 工作原理 协议篇 2-1 DDR4 引脚 设计篇 实践篇 进阶篇 学习路线: 了解DDR4的基本知识…

openh264 SVC 时域分层原理介绍

openh264 OpenH264是一个开源的H.264编码器,由Cisco公司开发并贡献给开源社区。它支持包括SVC(Scalable Video Coding)在内的多种编码特性,适用于实时应用场景,比如WebRTC。OpenH264项目在GitHub上是公开的&#xff0…

Docker部署常见应用之SFTP服务器

文章目录 简介Dockers部署单用户多用户用户信息加密使用SSH密钥认证 参考文章 简介 atmoz/sftp 是一个基于 Docker 的 SFTP 服务镜像,它使用 OpenSSH 来提供 SFTP 服务。这个镜像支持创建单个或多个用户的 SFTP 访问,并允许用户通过 SFTP 协议安全地共享…

Excel 多列组合内容循环展开

某表格 A 列是编号,其他列是用逗号分隔的意义不同的分类列 ABCDEFG1Assembly#ProductTypeUnit ConfigNominal CapacitySupply VoltageGenerationCase Construction23H1012290001CMD,P24,36FAA,B33H1012290002CMD,P48,60FA,BA,B43H1012290003CMD,P24,36B,C,D,EAA,B …

SSM母婴用品交流系统-计算机毕业设计源码05772

摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 母婴用品交流系统,主要的模块包括查看首页、轮播图管理、通知公告管理、资源管理(母婴资讯、资讯分类)、交…

统计信号处理基础 习题解答10-17

题目: 在选择不含信息的或者不假设任何先验知识的先验PDF时,我们需要从数据中得到最大的信息量。在这种方式下,数据是了解未知参数的主要贡献者。利用习题10.15的结果,这种方法可以通过选择使I最大的来实现。对于例10.1的高斯先验PDF,该如何选择和2使得 是不含信息…

visual studio 2022使用全版本平台工具集

https://www.cnblogs.com/coolfan/p/15822057.html vs2022使用全版本平台工具集 关键词:visual studio 2022使用全版本平台工具集;vs2022使用vc60、vc6.0工具集;vs2022使用全部旧平台工具集 优点:用v60平台工具集编译出来的exe文件可以无依赖的运行在W…

流量卡怎么办理的攻略

一、确定需求 在办理流量卡之前,你需要明确自己的需求。根据不同的使用场景,你可能需要考虑以下几个方面: 月租费用:不同运营商提供的流量卡套餐价格各异,从低至19元到高达199元不等。 流量大小:从30GB到3…

【设计模式深度剖析】【8】【行为型】【备忘录模式】| 以后悔药为例加深理解

👈️上一篇:观察者模式 设计模式-专栏👈️ 文章目录 备忘录模式定义英文原话直译如何理解呢? 3个角色1. Memento(备忘录)2. Originator(原发器)3. Caretaker(负责人)类…

使用 Python 进行测试(1)测试基础

原文 总结 我们将从unittest开始,尽管它并不那么好用,但它是Python标准库中的测试工具。 使用unittest编写测试看起来像这样: import unittest# 需要测试的代码 def add(a, b):return a b# The tests class TestAddFunction(unittest.Test…

拦截器 之 用户登录判断

spring boot 拦截器的实现需要有两步: 自定义一个拦截器 package com.example.demo.common;import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springfra…

Golang——gRPC gateway网关

前言 etcd3 API全面升级为gRPC后,同时要提供REST API服务,维护两个版本的服务显然不大合理,所以gRPC-gateway诞生了。通过protobuf的自定义option实现了一个网关。服务端同时开启gRPC和HTTP服务,HTTP服务接收客户端请求后转换为gr…

SAP HANA1709~2023版本Fiori激活简介

SAP Fiori 是一个设计系统,使您能够创建具有消费者级别用户体验的业务应用,通过在任何设备上运行,可以在Ipad或者是手机端都可以随时随地的使用SAP,现在越来越多的公司都在使用Fiori系统,公司高层可以更直观的在移动端设备中查看各种数据。 本文主要说明HANA版本怎么激活F…

直流放大器

一,概念及存在问题 集成电路主要由半导体材料构成,其内部适合用二极管,三极管等类型的元器件制作,而不适用电容,电感和变压器,因此集成放大电路内部多个放大电路之间通常采用直接耦合。直接耦合电路除了可…

牛客热题:兑换零钱(一)

📟作者主页:慢热的陕西人 🌴专栏链接:力扣刷题日记 📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言 文章目录 牛客热题:兑换零钱(一)题目链接方法一&am…

基于WPF技术的换热站智能监控系统03--实现左侧加载动画

1、左侧布局规划 左侧分5行,每行的高度通过height属性来指定,1.2*表示占1.2倍的宽度 2、创建用户控件 在WPF中想要进行个性化处理,主要可以通过三个方面来实现:控件模板(控件模板、数据模板、数据容器模板&#xff09…

【Webpack】使用 Webpack 构建 Vue3+TS 项目

构建项目目录 tsc --init npm init -yshim.d.ts 文件是一个类型声明文件,用于告诉 TypeScript 编译器如何处理 Vue 的单文件组件(SFC)和其他自定义模块。为 Vue 的单文件组件和其他非 TypeScript 模块提供类型信息,以便在 TypeScr…