[Java并发编程] Java开发必须知道的synchronized关键字

前言

        在Java后台开发或者Android开发中,synchronized出现的频率并不算低。本文就什么是synchronized,如何使用synchronized以及synchronized的实现原理做深入的讲解,揭开synchronized神秘面纱,有助于大家掌握synchronized的用法并深谙synchronized深层原理,提升并发编程能力,写出更高性能的程序。

什么是synchronized

        synchronized在Java中是以关键字的形式出现,英文直译多来就是“同步”,也被大家称作为同步锁或互斥锁。

        synchronized使用的场景都是在多线程并发的情况下,用来控制同步代码(方法)的执行,确保同一时刻只能有一个线程执行同步代码(方法),从而保证了共享资源在多线程下的数据一致性。

        下面用代码举个例子。在一个类中定义一个int变量和一个对该变量进行累加的方法,并实例化出一个类的对象,然后起两个线程分别在线程中对同一个对象执行10000次累加操作,最后输出对象中count的值。

        如果不去考虑并发编程的原子性和可见性导致的线程安全的问题,经过2个线程各10000次累加,最后的结果必然是20000,可是实际的输出结果往往到不了20000。 这就是多线程下同时去操作共享资源而导致的数据不一致性。设想下这个count如果代表的是商品的库存,多线程代表商家补货操作,这势必会造成严重的错误。那么synchronized的出现就有效了解决了此为数据不一致的问题。我们只需在add()方法前加一个synchronized修饰,如下图:

        这次的输出结果就准确无误了。

synchronized的作用

  • 内存可见性:确保当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。
  • 操作原子性:确保整个操作(可以是一个或多个步骤)作为一个整体被执行,其顺序不会被打乱或只执行部分操作。

        这里值得一提的是synchronized也是能够确保内存可见性(很多人不知道这点,以为只有volatile能保证可见性),但是synchronized无法禁止编译器或者处理器对指令的重排序,无法保证指令的有序性,必要的时候需要volatile关键字一起配合使用。

synchronized的使用方式

synchronized有两种使用方式,也可以说有三种,这里就都列出来:

  • 修饰代码块:使用格式为synchronized(lock){//代码块},其中lock是一个对象,被修饰的代码块称为同步代码块。当一个线程访问一个对象中的synchronized(lock)同步代码块时,其他试图访问该对象的同步代码块的线程将被阻塞。
public void add() {
	synchronized(lock) {
		count++;
	}
}
  • 修饰方法:使用格式为public synchronized void methodName(){//方法体},被修饰的方法称为同步方法。同步方法的作用范围是整个方法,作用对象是调用这个方法的对象。
public synchronized void add() {
	count++;
}
  • 修饰静态方法:使用格式为public synchronized void methodName(){//方法体},被修饰的方法称为同步静态方法。同步静态方法的作用范围是整个方法,作用对象是静态方法所在类的class对象。
public static synchronized void add() {
	count++;
}

        这里要解释下这个修饰代码块时传入的这个lock对象。很多新手开发可能对这个lock对象都很迷,不知道这个经常被叫做对象锁的东西是干什么的,也不知道什么传。

        有时候同步代码块里会直接传this(指向当前对象),有时候又会去实例化一个lock对象(new一个Object)传入。

public void add() {
	synchronized(this) {
		count++;
	}
}
Object lock = new Object();
public void add() {
	synchronized(lock) {
		count++;
	}
}

        这又有什么区别呢?其实这里只要确保需要同步的代码块或者方法使用的是同一个对象锁就可以。

        下面代码的这种方式是无法确保同步代码(add方法和reduce方法内同步代码块种的代码)的互斥访问的,原因是两个同步代码块使用的不是同一个对象锁。

Object lock = new Object();
	
public void add() {
	synchronized(lock) {
		count++;
	}
}
	
public void reduce() {
	synchronized(this) {
		count--;
	}
}

        同步实例方法(对象方法)使用的对象锁就是调用该方法的实例(对象)。

public class ConcurrentDeome_2 {
	// 多线程共享的变量
	private int count;

	//同步方法
	public synchronized void add() {
		count++;
	}

	public static void main(String[] args) throws InterruptedException {
		ConcurrentDeome_2 accumulator = new ConcurrentDeome_2();
		// add()同步方法使用的对象锁就是accumulator指向的对象
		accumulator.add();
	}
}

        同样同步静态方法使用的对象锁是该方法的类的class对象。

synchronized如何被编译

        大家应该知道,Java代码会被Java虚拟机编译成虚拟机能识别的字节码指令,当然字节码人类是没法阅读的,不过可以通过javap命令进行反编译出汇编指令就能读懂了。

        我们先来对普通的add方法进行Javap反编译。

public void add() {
	count++;
}

得到以下结果:

public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #17                 // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #17                 // Field count:I
        10: return

然后再对有synchronized修饰方法中的代码块的代码进行Javap反编译。

public void add() {
	synchronized(this) {
		count++;
	}
}

得到以下结果:

  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #17                 // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #17                 // Field count:I
        14: aload_1
        15: monitorexit
        16: goto          22
        19: aload_1
        20: monitorexit
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    21    19   any

乍一看多了加了一个synchronized包裹,多了好多字节码指令。后者将一系列的指令包裹在了指令monitorentermonitorexit之间。没错,在这个字节码示例中,字节码指令monitorentermonitorexit用于实现同步代码块的进入和退出。

为什么会有两个monitorexit指令?这通常是因为字节码中包含了异常处理逻辑。在Java字节码中,为了确保即使在发生异常的情况下也能正确释放锁,编译器会生成一个异常表(Exception table),并在异常处理路径上插入额外的monitorexit指令。

再来看看synchronized修饰方式时Javap反编译出的字节码指令。

public synchronized void add() {
	count++;
}

得到以下结果:

  public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #17                 // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #17                 // Field count:I
        10: return

方法的Code下还是和普通方法时反编译出来的结果一样,并没有看到有monitorenter和monitorexit指令,但是在flags下多了一个ACC_SYNCHRONIZED标识。Java虚拟机就是从方法常量池中的method_info Structure方法表结构中,靠ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法。

Java虚拟机(JVM)的锁机制

        到这里我们已经知道当Java代码中使用synchronized关键字时,Java编译器会将其编译成特定的字节码指令,以确保同步的正确实现。在字节码层面,synchronized的实现涉及到JVM的锁机制。

       JVM的锁机制和上面提到的进入和退出monitor的两条指令息息相关。要理解Java虚拟机的锁机制的前提是认识这个monitor。

        monitor直译过来就是监视,我们通常称它为监视器锁也可以称作管程(学过操作系统的应该对它不陌生),它以对象的形式存储于Java堆内存中。Java中每个对象都伴生着自己的Monitor对象,每一个synchronized修饰的同步代码块使用的对象锁或者调用synchronized修饰的方法的对象都会和一个monitor关联(对象锁的对象头的MarkWord中的ptr_to_heavyweight_monitor指向堆内存中monitor的起始地址)。可以说Java对象头是synchronized底层实现的关键要素。

        个人觉得monitor可以理解为用来管理线程同步(管程嘛)。不同的Java虚拟机可能对monitor的实现略有差异,通常来说monitor下面有这么几个字段(以下是伪代码,只列部分字段):

	Monitor {
		_count = 0;       //记录个数
		_recursions = 0;  //重入次数
		_waiters = 0;     //等待线程数
		_owner = null;    //指向当前所属的线程
	}

        知道了什么是monitor后,现在开始讲解Java虚拟机实现锁机制的重点:

        线程执行monitorenter指令尝试去获取监视器锁的虚拟机层的实现就是先会去判断Monitor对象下count字段的值是否为0,如果count等于0,将count加1,owner赋值当前线程起始地址,说明成功获取锁,然后开始执行加锁部分的同步代码指令;如果count不等于0,并且owner并没有指向线程自己,那么此时线程将进入阻塞状态(可以理解为被丢到了一个阻塞队列中去了),等待锁的释放;还有一个情况是如果count不等于0,并且owner指向线程自己,此时是允许该线程再次获取锁的,count再加1,recursions也会加1(synchronized是可重入锁)。

        线程执行monitorexit指令释放锁,Monitor对象的count减1,如果是重入锁recursions也跟着自减,owner置空。此时Monitor对象没有被任何线程占有,在阻塞队列中等待获取锁的线程都有机会获取到锁(synchronized是非公平锁)。对于非公平锁的解释就是:当一个线程释放锁时,不一定是等待时间最长的线程获得锁,而是由Java虚拟机随机选择一个等待线程来获取锁。这样就存在某些线程等待时间较长的情况,导致不公平。

        此外,还有一个关键的字段waiters,当线程在执行同步的代码时,调用了锁对象的wait()方法,此时线程也会释放锁,Monitor的waiters加1,线程被丢到一个等待队列,直到调用了锁对象的notify()或notifyAll()方法后,线程才会从等待队列中被移除并添加到阻塞队列和其它线程竞争获取锁。

        到这里Java虚拟机的锁机制已经将的差不多了,这种机制的作用是确保在多线程环境下,能够避免多个线程同时访问同一个共享资源,从而防止线程安全问题。当一个线程试图访问一个被另一个线程占用的对象时,它会被阻塞,直到监视器被释放为止。

synchronized的缺点

         synchronized是非公平锁,无法做到先到先得。

         前面说的Monitor监视器锁的本质是依赖于底层操作系统的Mutex Lock实现,因此虚拟机在执行获取锁和释放锁的指令时操作系统需要进行用户态线程和内核态线程的相互切换,频繁的切换势必会损耗性能影响程序执行速度。因此,早期的synchronized属于重量级锁,存在效率低下的问题。

JDK1.6对synchronized的优化

        JDK 1.6对synchronized进行了多项优化,这些优化主要是为了提升多线程环境下同步代码的性能。以下是JDK 1.6对synchronized的主要优化:

  1. 自适应自旋锁
    • 在JDK 1.5及以前,synchronized关键字的锁机制在竞争锁时,如果当前线程没有获取到锁,就会进入阻塞状态,直到获取到锁。这种方式存在线程挂起和恢复的开销,在竞争激烈的情况下会频繁发生,从而严重影响性能。
    • 在JDK 1.6中,引入了自适应自旋锁,即当线程尝试获取锁失败时,不是立即阻塞,而是采用自旋的方式等待一段时间,再次尝试获取锁。如果在这段时间内锁被释放了,那么当前线程就可以获取到锁,避免了线程挂起和恢复的开销。
    • 自适应自旋锁会根据历史信息(如自旋成功的次数和失败的比例)来动态调整自旋的次数,如果自旋成功率高,则增加自旋次数,反之则减少自旋次数,甚至直接阻塞。
  2. 锁消除
    • 锁消除是JIT编译器在编译时,对代码进行扫描,如果发现某些共享数据不可能被并发修改,那么就可以将其上的锁消除掉,从而减少不必要的同步开销。
  3. 锁粗化
    • 如果存在连续多次对同一个对象加锁和解锁的操作,JDK 1.6会将这些锁操作合并成一个更大的锁范围,以减少锁操作的次数,这就是锁粗化。
  4. synchronized锁的膨胀其主要目的是为了提高同步控制的效率。这个过程涉及到锁状态的升级,从低级的锁状态升级到高级的锁状态:

  • 无锁状态:当一个线程尝试获取一个对象的锁时,如果该对象的锁状态为无锁状态(unlocked),则进入下一步。

  • 偏向锁:
    • 偏向锁是JDK 1.6引入的一种锁优化机制。它的核心思想是,如果一个线程获得了某个对象的锁,那么在下一次访问该对象时,就无需再次进行锁的竞争,而是直接偏向这个线程。
    • 在偏向锁的获取过程中,首先会检查对象头中的锁标志位是否为偏向模式。如果是,则检查持有偏向锁的线程是否就是当前线程。如果是,则直接获得锁;如果不是,则通过CAS操作尝试获取锁。如果CAS失败,则偏向锁升级为轻量级锁。
  • 轻量级锁:
    • 如果偏向锁获取失败,线程会尝试获取轻量级锁。此时,会使用CAS操作将对象的锁状态设置为轻量级锁状态(lightweight locked),并将当前线程ID记录在对象头中。
    • 如果CAS操作成功,则当前线程就拥有了该对象的轻量级锁,并可以直接访问该对象。
    • 如果CAS操作失败,表示该对象已经被其他线程获取了轻量级锁。此时,当前线程会进入自旋状态,使用自旋锁等待其他线程释放该对象的轻量级锁。
  • 锁膨胀为重量级锁:
    • 如果自旋等待超过了一定的次数或时间,JVM就会将该对象的锁状态升级为重量级锁状态(heavyweight locked)。此时,当前线程会进入阻塞状态,并将自己加入到该对象的等待队列中,等待其他线程释放该对象的锁。
    • 重量级锁会阻塞、唤醒请求加锁的线程,它针对的是多个线程同时竞争同一把锁的情况。

        这些优化使得JDK 1.6中的synchronized关键字在多线程环境下表现更为出色,提高了并发性能。需要注意的是,虽然这些优化能够提升性能,但并发编程仍然是一个复杂的领域,需要谨慎处理同步和锁的使用,以避免出现死锁、活锁等问题。

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

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

相关文章

生成式AI来袭,FOSS全闪对象存储应时而生

AI大模型正飞速跃进,从引领文本生成革命的ChatGPT到开创文生视频新纪元的Sora,多模态交互技术连续迭代,促进了智算中心的快速落地。在这一过程中,算力的迅猛增长对存储系统提出了更高的要求和挑战。为满足这些日益增长的需求&…

校园生活信息平台:Java+Vue+MySQL全栈实践

✍✍计算机编程指导师 ⭐⭐个人介绍:自己非常喜欢研究技术问题!专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目:有源码或者技术上的问题欢迎在评论区一起讨论交流! ⚡⚡ Java实战 |…

机界先锋:Figure 01实现全面沟通与AGI通用人工智能的征途

✨✨ 欢迎大家来访Srlua的博文(づ ̄3 ̄)づ╭❤~✨✨ 🌟🌟 欢迎各位亲爱的读者,感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢,在这里我会分享我的知识和经验。&am…

idea安装了某个插件之后启动报错打不开怎么办

刚才安装了个这个插件 然后重启的时候就报错了 如下 不用慌,看一下报错日志,找到插件的位置给他删了就行了,往下拉一点 找到这个文件给他删了,再启动就好了。记得删之前先把上面这个报错弹框关了,不然会提示文件占用不…

阿里云第一次面试记录

java多态? 多态表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例 Fu f Zi z(); 多态是同一个行为具有多个不同表现形式或形态的能力。 多态就是同一个接口,使用不同的实例而执行不同操作 特点: 对象类型和引用类型…

wordpress博客趣主题个人静态网页模板

博客趣页面模板适合个人博客,个人模板等内容分享。喜欢的可以下载套用自己熟悉的开源程序建站。 博客趣主题具有最小和清洁的设计,易于使用,并具有有趣的功能。bokequ主题简约干净的设计、在明暗风格之间进行现场切换。 下载地址 清新个人…

未来已来:科技驱动的教育变革

我们的基础教育数百年来一成不变。学生们齐聚在一个物理空间,听老师现场授课。每节课时长和节奏几乎一致,严格按照课表进行。老师就像“讲台上的圣人”。这种模式千篇一律,并不适用于所有人。学生遇到不懂的问题,只能自己摸索或者…

ctf_show笔记篇(web入门---代码审计)

301:多种方式进入 从index.php页面来看 只需要访问index.php时session[login]不为空就能访问 那么就在访问index.php的时候上传login 随机一个东西就能进去从checklogin页面来看sql注入没有任何过滤 直接联合绕过 密码随意 还有多种方式可以自己去看代码分析 30…

【OpenCV实战】基于OpenCV中DNN(深度神经网络)使用OpenPose模型实现手势识别详解

一、手部关键点检测 如图所示,为我们的手部关键点所在位置。第一步,我们需要检测手部21个关键点。我们使用深度神经网络DNN模块来完成这件事。通过使用DNN模块可以检测出手部21个关键点作为结果输出,具体请看源码。 二,openpose手势识别模型 OpenPose的原理基于卷积神经网…

购物平台为何要添增短视频部件?是应时代发展还是别有用心?

每天五分钟讲解一个商业模式,大家好我是模式设计啊浩。 不知道大家有在购物商城平台刷过短视频吗,不管怎么说啊浩是有这样做过的,甚至一度觉得还挺有意思的。有时候本来只是想去买件衣服,结果商城刷了五分钟,短视频就刷…

SpringBoot配置达梦数据库依赖(达梦8)

maven配置 <!-- 达梦数据库 --><dependency><groupId>com.dameng</groupId><artifactId>DmJdbcDriver18</artifactId><version>8.1.1.193</version></dependency><dependency><groupId>com.alibaba&l…

利用位运算符设置标志位

在写程序的过程中&#xff0c;会碰到需要修改标志位的情况。比如需要设置一个文件标识符可读或可写&#xff0c;首先想到的是利用int变量&#xff08;1表示不可读不可写 &#xff0c;2表示不可读可写&#xff0c;3表示可读不可写&#xff0c;4表示可读可写&#xff09;。但是这…

ChatGPT提问技巧——对话提示

ChatGPT提问技巧——对话提示 对话提示是一种允许模型生成模拟两个或多个实体之间对话的文本的技术。 通过向模型提供上下文和一组角色或实体&#xff0c;以及他们的角色和背景&#xff0c;并要求模型生成他们之间的对话。 因此&#xff0c;应向模型提供一个上下文和一组角色…

中国工程精英智创数字工厂——2023纵览基础设施大会暨光辉大奖赛观察 (下)

中国工程精英智创数字工厂 ——2023纵览基础设施大会暨光辉大奖赛观察 &#xff08;下&#xff09; 吴付标 中国制造的尽头是智能化、智慧化&#xff0c;这一趋势正在加速前进。2022年&#xff0c;中国以50座达沃斯论坛盖章认证的“灯塔工厂”数量冠绝全球&#xff0c;而“数…

Javascript的Execution Context

概要 本文主要通过一个实例&#xff0c;来理解什么是Javascript中的Execution Context&#xff0c;以及在JavaScript执行过程中&#xff0c;Execution Context是如何工作的。 基本概念 事实上&#xff0c;我们可以理解为JavaScript代码在一个盒子中执行&#xff0c;而这个盒…

Linux 建立链接(ln)

目录 1、ln命令 创建软链接&#xff1a; 创建硬链接&#xff1a; 2、输出重定向&#xff08;>/>>&#xff09; 3、管道&#xff08;|&#xff09; 1、ln命令 &#xff08;英文全拼&#xff1a;link files&#xff09;为某一个文件在另外一个位置建立一个同步的…

如何使用vue定义组件之——子组件调用父组件数据

1.定义父子模板template <div class"container"><my-father></my-father><my-father></my-father><my-father></my-father><!-- 此处无法调用子组件&#xff0c;子组件必须依赖于父组件进行展示 --><!-- <my-…

如何实现sam(Segment Anything Model)|fastsam模型

sam是2023年提出的一个在图像分割领域的大模型&#xff0c;其具备了对任意现实数据的分割能力&#xff0c;其论文的介绍可以参考 https://hpg123.blog.csdn.net/article/details/131137939&#xff0c;sam的亮点在于提出一种工作模式&#xff0c;同时将多形式的prompt集成到了语…

武汉星起航:秉承客户至上服务理念,为创业者打造坚实后盾

在跨境电商的激荡浪潮中&#xff0c;武汉星起航电子商务有限公司一直秉持着以客户为中心的发展理念&#xff0c;为跨境创业者提供了独特的支持和经验积累&#xff0c;公司通过多年的探索和实践&#xff0c;成功塑造了一个以卖家需求为导向的服务平台&#xff0c;为每一位创业者…

专升本 C语言笔记-03 变量的作用域

1.变量的概念 内存中有个存储区域,这个地方的数据可以在同一类型范围内不断变化通过变量名,可以访问这块内存区域,获取里面的值; 变量名的构成:数据类型 变量名 值 C语言中变量声明格式: 数据类型 变量名 值 2.变量的注意 2.1.全局变量: 定义在函数外部的叫全局变量…