深入理解 Java 中的 synchronized 代码块

news2025/5/19 11:33:07

目录

前言

一、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() {
			@Override
			public void run() {
				otz.doLongTimeTask();
			}
		},"t1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public 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() {
			@Override
			public void run() {
				objLock.method1();
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				objLock.method2();
			}
		});
		Thread t3 = new Thread(new Runnable() {
			@Override
			public 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() {
			@Override
			public void run() {
				stringLock.method();
			}
		},"t1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public 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() {
			@Override
			public void run() {
				changeLock.method();
			}
		},"t1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public 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() {
			@Override
			public void run() {
				modifyLock.changeAttributte("张三", 20);
			}
		},"t1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public 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;
	}
	
	@Override
	public 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.coloradmin.cn/o/1826373.html

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

相关文章

大众点评js逆向过程(未完)

1、这里mtgsig已经被拼到url中 2、进入后mtgsig已经计算完, ir he(this[b(4326)], !1), 就是加密函数 32 次 796 1143 ->508 -> 754 -> 1151 160 注意IC这个数组 控制流平坦化进行AST 解析 AST网址

【背包题】oj题库

目录 1282 - 简单背包问题 1780 - 采灵芝 1888 - 多重背包&#xff08;1&#xff09;​编辑 1891 - 开心的金明 2073 - 码头的集装箱 1905 - 混合背包 1282 - 简单背包问题 #include <bits/stdc.h> using namespace std; //二维数组:dp[i][j]max(dp[i-1][j],v[i]dp[…

利用485缓存器实现两主一丛RS485串行通信

作者:艺捷自动化&#xff0c;其旗下产品有艺捷自动化网站和易为二维码小程序&#xff08;微信&#xff09; 对于工控自动化领域的电气工程师来说&#xff0c;基于RS485的串行通讯是最常见的。绝大部分仪表都能支持这种通讯方式。RS485通讯&#xff0c;是一种异步半双工模式&…

民生银行信用卡中心金融科技24届春招面经

本文介绍2024届春招中&#xff0c;中国民生银行下属信用卡中心的金融科技&#xff08;系统研发方向&#xff09; 岗位2场面试的基本情况、提问问题等。 2024年04月投递了中国民生银行下属信用卡中心的金融科技&#xff08;系统研发方向&#xff09; 岗位&#xff0c;暂时不清楚…

【Linux内核】伙伴系统算法和slab分配器(1)

【Linux内核】伙伴系统算法和slab分配器&#xff08;1&#xff09; 目录 【Linux内核】伙伴系统算法和slab分配器&#xff08;1&#xff09;伙伴系统&#xff08;buddy&#xff09;算法伙伴系统算法基本原理内存申请内存回收 接口函数源码分析内存分配接口物理内存释放接口规范…

spring boot配置ssl证书,支持https访问

1. 阿里云官网下载证书,云控制台搜索ssl&#xff0c;点击进入。 2.点击免费证书&#xff0c;立即购买。 3. 点击创建证书&#xff0c;填写完证书申请后&#xff0c;等待证书签发。 4. 证书签发以后&#xff0c;点击下载证书&#xff0c;spring boot选tomcat服务器类型的。 5. …

FineReport简单介绍

一、介绍 官网 &#xff1a;FineReport产品简介- FineReport帮助文档 - 全面的报表使用教程和学习资料 报表是以表格、图表的形式来动态展示数据&#xff0c;企业通过报表进行数据分析&#xff0c;进而用于辅助经营管理决策。 FineReport 是一款用于报表制作&#xff0c;分析和…

Mybatis-Plus多种批量插入方案对比

背景 六月某日上线了一个日报表任务&#xff0c;因是第一次上线&#xff0c;故需要为历史所有日期都初始化一次报表数据 在执行过程中发现新增特别的慢&#xff1a;插入十万条左右的数据&#xff0c;SQL执行耗费高达三分多钟 因很早就听闻过mybatis-plus的[伪]批量新增的问题&…

C++语法05 浮点型/实数类型

什么是实数类型 实数类型是一种数据类型&#xff0c;实数类型变量里能存放小数和整数。 定义格式&#xff1a;double a; 赋值&#xff1a;a0.4; 输入&#xff1a;cin>>a; 输出&#xff1a;cout<<a; 训练&#xff1a;尺子的价格 小知在文具店买铅笔&#xff…

亲测几十款随身wifi,全网最全随身WiFi避坑指南!最值得买的随随身wifi品牌推荐!

关于随身wifi我认为我是比较有发言权的&#xff0c;历经三年测评了几十种随身wifi&#xff0c;便宜的贵的&#xff0c;大牌的小厂的&#xff0c;电池款USB款等各种随身wifi。根据测试结果以及通过电商平台搜索、粉丝反馈、社交平台评价等综合测评结果。今天就跟大家分享一下&am…

Orange_Pi_AIpro运行蜂鸟RISC-V仿真

Orange_Pi_AIpro运行蜂鸟RISC-V仿真 突发奇想&#xff0c;试一试Orange Pi AIpro上运行蜂鸟RISC-V的仿真。 准备 默认已经有一个Orange Pi AIpro&#xff0c;并且对设备进行一定的初始化配置&#xff0c;可以参考上一篇博文开源硬件初识——Orange Pi AIpro&#xff08;8T&a…

《C++ Primer》导学系列:第 2 章 - 变量和基本类型

2.1 基本内置类型 概述 本小节介绍C中的基本内置类型。基本内置类型是构成C程序的基础&#xff0c;它们用于表示数据的不同形式&#xff0c;如整数、浮点数、字符等。理解和使用这些基本类型是编写C程序的起点。 2.1.1 算术类型 C的算术类型分为整型和浮点型。整型用于表示…

LVS三种负载均衡模式:NAT、Tunneling和DR的技术对比

1. LVS-NAT 模式的特性 IP使用&#xff1a;RS&#xff08;Real Server&#xff09;应使用私有地址&#xff0c;RS的网关必须指向DIP&#xff08;Director IP&#xff09;。网络范围&#xff1a;DIP和RIP必须在同一个网段内。数据包处理&#xff1a;请求和响应报文都需要经过Di…

【YashanDB知识库】PHP使用OCI接口使用数据库绑定参数功能异常

【问题分类】驱动使用 【关键字】OCI、驱动使用、PHP 【问题描述】 PHP使用OCI8连接yashan数据库&#xff0c;使用绑定参数获取数据时&#xff0c;出现报错 如果使用PDO_OCI接口连接数据库&#xff0c;未弹出异常&#xff0c;但是无法正确获取数据 【问题原因分析】 开启O…

远程桌面失败:你的凭据不工作

远程桌面失败&#xff1a;你的凭据不工作 远程桌面失败&#xff1a;你的凭据不工作_您的凭据不工作-CSDN博客https://blog.csdn.net/weixin_38004638/article/details/82290796

java设计模式和面向对象编程思想

Java设计模式和面向对象编程思想是软件开发中的核心概念&#xff0c;对于构建可维护、可扩展的软件系统至关重要。下面是对这两个主题的知识点总结&#xff1a; 面向对象编程&#xff08;OOP&#xff09;思想 封装&#xff1a;将数据&#xff08;属性&#xff09;和操作这些数据…

Linux C编译器从零开发一

基础程序汇编 test.c int main() {return 42; } 查看反汇编 cc -o test test.c objdump -d -M intel test 0000000000001129 <main>:1129: f3 0f 1e fa endbr64 112d: 55 push rbp112e: 48 89 e5 mov rbp,rsp1131: b…

攻防世界-fakebook题目__详解

1.打开题目先用dirsearch工具扫描一波&#xff0c;扫出来了robots.php目录&#xff0c;然后访问robots.txt 目录&#xff0c;发现了有一个备份文件 &#xff0c;访问备份文件&#xff0c;下载内容 文件的大致内容如下 里面有一个curl_exec这个函数容易造成ssrf攻击的漏洞 我…

运行时类型识别RTTI(typeid dynamic_cast)和虚函数机制的关系

1.typeid 2.dynamic_cast 指针类型决定了可以操作的内存范围大小 子类指针转化为父类类型的指针的一般是合法的&#xff1a; 父类的指针类型转化为子类类型指针&#xff0c;超过合法操作范围&#xff0c;不安全 两种转换&#xff1a;编译期的转换&#xff0c;运行时的转化 编译…

四川汇聚荣聚荣科技有限公司是干什么的,拼多多运营如何做?

四川汇聚荣聚荣科技有限公司是干什么的&#xff0c;拼多多运营如何做?随着电商行业的快速发展&#xff0c;越来越多的企业开始涉足这一领域。其中&#xff0c;四川汇聚荣聚荣科技有限公司便是其中的一员。那么&#xff0c;这家公司究竟是做什么的呢?简单来说&#xff0c;它是…