当前位置: 首页 > 科技观察

synchronized天天用,实现原理你懂吗?

时间:2023-03-12 12:46:01 科技观察

synchronized关键字被誉为Java的老牌锁。一开始是支持Java的同步任务。它的用法简单易用。但是与之相关的一些知识点还是需要我们开发者去掌握的。比如我们都知道互斥功能是通过Synchronized锁来实现的,可以用在方法或者代码块中,所以我们需要扎实的了解不同的用法是如何实现的,经历了哪些优化。1.基本用法通常我们可以在方法或者代码块中使用Synchronized,方法有普通方法或者静态方法。对于普通同步方法,锁是当前实例对象,即thispublicclassTestSyn{privateinti=0;publicsynchronizedvoidincr(){i++;}}对于静态同步方法,锁是Class对象publicclassTestSyn{privatestaticinti=0;publicstaticsynchronizedvoidincr(){i++;}}对于同步代码块,锁是同步代码块中的对象publicclassTestSyn{privateinti=0;Objecto=newObject();publicvoidincr(){synchronized(o){i++;}}}2.实现原理Synchronized在JVM规范中介绍,JVM的实现原理是根据进入和退出Monitor对象来实现方法同步和代码块同步,但是两者的实现细节不同。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是通过另一种方式实现的,通过一个方法标志(flag)ACC_SYNCHRONIZED。1、同步代码块的实现(一)monitorenter和monitorexithttps://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.monitorenter(参考源)见下JVM规范中monitorenter和monitorexit的介绍:每个对象都有一个与之关联的monitor。执行monitorenter的线程获得与objectref关联的监视器的所有权。如果另一个线程已经拥有与objectref关联的monitor,则当前untilthreadilwaits对象被解锁,每个对象都有一个与之关联的moniter(监视器),执行m??onitenter指令的线程将取得与objectref关联的monitor的所有权,如果另一个线程已经拥有与objectref关联的监视器,那么当前线程将等待直到对象被解锁。monitorenter指令可以与一个或多个monitorexit指令一起使用,以实现Java编程语言中的同步语句。monitorenter和monitorexit指令在synchronized方法的实现中没有使用monitorenter和monitorexit指令用于实现Java语言的同步代码块(下面有代码示例)monitorenter和monitorexit指令在同步方法中没有使用!!!2.同步方式的实现先看JVM规范中是怎么说的:https://docs.oracle.com/javase/specs/jvms/se6/html/Compiling.doc.html#6530(参考来源)同步方法通常不使用monitorenter和monitorexit实现。相反,它只是在运行时常量池中通过ACC_SYNCHRONIZED标志进行区分,该标志由方法调用指令检查。当调用一个设置了ACC_SYNCHRONIZED的方法时,当前线程获取一个监视器,调用方法本身,是否调用方法释放监视器正常或突然完成。上面这段话主要讲了几点:同步方法的实现并不是基于monitorenter和monitorexit指令。在运行时常量池中,通过ACC_SYNCHRONIZED来区分是否是同步方法。执行方法时将检查此标志。当一个方法有这个标志时,进入的线程首先需要获得监视器来执行该方法。方法结束或抛出异常时会释放监听publicclassTestSyn{privateinti=0;//同步方法publicsynchronizedvoidincer(){i++;}//同步代码块publicvoiddecr(){synchronized(this){i--;}}}可以反编译字节码看看底层是怎么实现的//获取字节码javacTestSyn.java//反编译字节码javap-vTestSyn.class同步代码块的反编译结果如下:同步方式如下:三、锁升级1、Java对象头介绍(一)对象的内存布局在我们常见的HotSpot虚拟机中,对象由三部分组成组件分别是对象头、实例数据、对齐填充。对象头是与锁信息相关的部分。对象的运行时数据会存储在对象头中,包括哈希码、GC分代年龄、锁状态(无锁、偏向锁、轻量级锁、重量级锁)、偏向锁、偏向线程ID等信息。存放上述内容的区域称为MarkWord。除了objectheader这部分之外,还有一部分区域用来存放typepointer,通过它可以定位到object的metadata信息。我们重点关注对象头的内存布局,因为这部分和我们这次有关系。一个对象在内存中的表示如下图:对象头的结构如下图所示:markword如下图表示:2.什么是锁升级下面以抓取一个对象为例厕所讲解锁升级过程。(1)当只有一个线程访问时,称为偏向锁。假设我们每个厕所都有一把钥匙。要使用厕所,我们必须先获得一把锁。一天早上,员工A匆忙刷卡去厕所,并在厕所门上贴上了“007工号在用”的标签,表示目前有007工号的员工占用(相当于到线程ID)。当他再次进入时,只要上面的标签还显示着工号007,他就可以随意进入,无需再次锁定。它有点偏向工作编号为007的员工,所以这被称为偏向锁。(2)当发生竞争时,升级为轻量级锁(自旋等待)。员工A在上厕所的时候,又来了两个人,想上厕所,发现厕所被别人用了,拿不到锁。所以他们只能在外面等着A出来。等待的过程称为“自旋”,这称为轻量级锁。那么还有一个问题,A出来后等待的两个人,谁会被活活锁死?有两种方式,按到达顺序排队或者不排队,这两种方式都可以实现,前者称为公平锁,后者称为非公平锁。(3)在自旋等待没有结果的时候升级到重量级锁,但是两人自旋一段时间后发现A还没出来(JDK1.向上升级,请教厕所管理员(操作系统)用于反馈,并升级为重量级锁。总共有四种锁状态,无锁状态,偏向锁,轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级为轻量级锁,然后升级为重量级锁。同时关注公众号Java技术栈,回复JVM46可获得46页的JVM调优教程。锁升级过程中markword的变化如下:(4)偏向lock偏向锁也是JDK1.6引入的一种锁优化,引入是为了在没有锁竞争的场景下优化锁消除,比如一段同步代码一直被单线程调用,在这种场景下,没有需要使用一个同步锁。这里所说的同步锁不是指synchronized,而是指操作系统层面的mutex。偏向锁的偏向性是指同步代码会一直偏向第一个调用它的线程,直到其他线程来竞争锁。当同步代码第一次被调用并获取到锁时,会在对象头和栈帧中加锁偏线程Id保存在记录行(LockRecord)中,线程不需要重新申请当它进入这里时的锁。只需要检测指向线程的ID是否存储在对象头的MarkWord中。偏向锁会取消偏向,直到另一个线程竞争锁。(5)轻量级锁轻量级锁是JDK1.6中新增的一种锁机制。其名称中的“轻量级”是相对于所使用的操作系统而言的。对于通过互斥实现的传统锁而言,传统的锁机制被称为“重量级”锁。它不是用来替代重量级锁的,它的初衷是为了减少传统重量级锁中使用操作系统互斥锁带来的性能消耗。在线程执行同步块之前,JVM会先在当前线程的栈帧中创建一个存放锁记录的空间,并将对象头中的MarkWord复制到锁记录中,官方称之为DisplacedMarkWord.然后该线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,则当前线程获取锁。如果失败,说明有其他线程在竞争锁,当前线程尝试使用自旋来获取锁。一直在原地旋转,如果旋转次数达到10次,就会升级为重量级锁。(6)竞争重量级锁的线程自旋一段时间后,会升级为重量级锁,获取锁失败。这时,锁的获取和释放将由操作系统来分配。系统会唤醒所有被阻塞的线程,进入新一轮的竞争模式。需要注意的是,这些被阻塞的线程并没有获得锁的优先级,也就是说synchronized锁是不公平的。另外,synchronized对中断操作也很不敏感,不会因为被中断而放弃阻塞等待。它要么获得锁,要么一直阻塞。