网站建设与网站设计哪个好学,南宁网站建设 超博网络,汕头网站建设技术支持,wordpress child theme文章目录 1. synchronized锁优化背景2. synchronized锁性能优化过程2.1 java5以前2.2 monitor锁2.3 java6开始 3. 无锁4. 偏向锁4.1 背景4.2 理论落地4.3 技术实现4.4 偏向锁的撤销4.5 题外话 5. 轻量级锁5.1 轻量级锁的加锁5.2 轻量级锁的释放5.3 锁升级 6. 重量级锁7. 锁升级… 文章目录 1. synchronized锁优化背景2. synchronized锁性能优化过程2.1 java5以前2.2 monitor锁2.3 java6开始 3. 无锁4. 偏向锁4.1 背景4.2 理论落地4.3 技术实现4.4 偏向锁的撤销4.5 题外话 5. 轻量级锁5.1 轻量级锁的加锁5.2 轻量级锁的释放5.3 锁升级 6. 重量级锁7. 锁升级与hashCode8. 总结9. JIT编译器对锁的优化9.1 锁消除9.2 锁粗化 1. synchronized锁优化背景
【强制】高并发时同步调用应该去考量锁的性能损耗。能用无锁数据结构就不要用锁能锁区块就不要锁整个方法体;能用对象锁就不要用类锁。
说明: 尽可能使加锁的代码块工作量尽可能的小避免在锁代码块中调用 RPC 方法
用锁能够实现数据的安全性但是会带来性能下降。无锁能够基于线程并行提升程序性能但是会带来安全性下降。如何求得平衡
锁的升级过程 无锁偏向锁轻量级锁重量锁
synchronized锁由对象头中的Mark Word根据锁标识位的不同而被复用及锁升级策略 2. synchronized锁性能优化过程
2.1 java5以前 只有Synchronized这个是操作系统级别的重量级操作 重量级锁在锁的竞争比较激烈的情况下性能会下降 用户态和内核态之间的切换
java的线程是映射到操作系统原生线程之上的如果要阻塞或唤醒一个线程就需要操作系统介入需要在户态与核心态之间切换这种切换会消耗大量的系统资源因为用户态与内核态都有各自专用的内存空间专用的寄存器等用户态切换至内核态需要传递给许多变量、参数给内核内核也需要保护好用户态在切换时的一些寄存器值、变量等以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中synchronized属于重量级锁效率低下因为监视器锁(monitor)是依赖于底层的操作系统的MutexLock(系统互斥量)来实现的挂起线程和恢复线程都需要转入内核态去完成阳寒或唤醒一个Java线程需要操作系经切换CPU状态来完成这种状态切换需要耗费处理器时间如果同步代码块中内容过于简单这种切换的时间可能比用户代码执行的时间还长”时间成本相对较高这也是为什么早期的synchronized效率低的原因
2.2 monitor锁 Monitor可以理解为一种同步工具也可理解为一种同步机制常常被描述为一个Java对象。Java对象是天生的Monitor每一个Java对象都有成为Monitor的潜质因为在Java的设计中每一个Java对象自打娘胎里出来就带了一把看不见的锁它叫做内部锁或者Monitor锁。 Monitor是在jvm底层实现的底层代码是c。本质是依赖于底层操作系统的Mutex Lock实现操作系统实现线程之间的切换需要从用户态到内核态的转换状态转换需要的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作。 Monitor与java对象以及线程是如何关联 如果一个java对象被某个线程锁住则该java对象的Mark Word字段中LockWord指向monitor的起始地址Monitor的Owner字段会存放拥有相关联对象锁的线程id
2.3 java6开始
为了减少获得锁和释放锁所带来的性能消耗引入了轻量级锁和偏向锁
偏向锁Mark Word存储的是偏向的线程ID轻量级锁Mark Word存储的是指向线程栈中Lock Record的指针重量级锁Mark Word存储的是指向堆中的monitor对象的指针 3. 无锁
public class Main {public static void main(String[] args) {Object obj new Object();System.out.println(ClassLayout.parseInstance(obj).toPrintable());System.out.println(16进制: Integer.toHexString(obj.hashCode()) \n);System.out.println(2进制: Integer.toBinaryString(obj.hashCode()) \n);System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
}看value时需要从右往左从下往上看字节顺序比特顺序则从左往右看
java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes total16进制:f2a0b8e2进制:1111001010100000101110001110java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 8e 0b 2a (00000001 10001110 00001011 00101010) (705400321)4 4 (object header) 0f 00 00 00 (00001111 00000000 00000000 00000000) (15)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes total4. 偏向锁
偏向锁:单线程竞争
当线程A第一次竞争到锁时通过操作修改Mark Word中的偏向线程ID、偏向模式。如果不存在其他线程竞争那么持有偏向锁的线程将永远不需要进行同步。
主要作用当一段同步代码一直被同一个线程多次访问由于只有一个线程那么该线程在后续访问时便会自动获得锁而无需进行加锁及释放锁
4.1 背景
Hotspot 的作者经过研究发现大多数情况下: 多线程的情况下锁不仅不存在多线程竞争还存在锁由同一个线程多次获得的情况偏向锁就是在这种情况下出现的它的出现是为了解决在只有在一个线程执行同步时提高性能
小总结
偏向锁会偏向于第一个访问锁的线程如果在接下来的运行过程中该锁没有被其他的线程访问则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句懒的连CAS操作都不做了直接提高程序性能
4.2 理论落地
在实际应用运行过程中发现“锁总是同一个线程持有很少发生竞争”也就是说锁总是被第一个占用他的线程拥有这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时不需要再次加锁和释放锁。而是直接会去检查锁的Mark Word里面是不是放的自己的线程ID)。
如果相等表示偏向锁是偏向于当前线程的就不需要再尝试获得锁了直到竞争发生才释放锁。以后每次同步检查锁的偏向线程ID与当前线程ID是否一致如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个很明显偏向锁凡乎没有额外开销性能极高。如果不等表示发生了竞争锁已经不是总是偏向于同一个线程了这个时候会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID竞争成功表示之前的线程不存在了 Mark Word里面的线程ID为新线程的ID锁不会升级仍然为偏向锁竞争失败这时候可能需要升级变为轻量级锁才能保证线程间公平竞争锁
注意偏向锁只有遇到其他线程尝试竞争偏向锁时持有偏向锁的线程才会释放锁线程是不会主动释放偏向锁的。
4.3 技术实现
一个synchronized方法被一个线程抢到了锁时那这个方法所在的锁对象就会将其所在的Mark Word中将偏向锁修改状态位同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID无需再进入 Monitor 去竞争对象了。
偏向锁的操作不用直接捅到操作系统不涉及用户到内核转换不必要直接升级为最高级我们以一个account对象的“对象头”为例 假如有一个线程执行到synchronized代码块的时候JVM使用CAS操作把线程指针ID记录到Mark Word当中并修改偏向标示标示当前线程获得该锁。锁对象变成偏向锁(通过CAS修攻对象头里的锁标志位》字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后线程并不会主动释放偏向锁。 这时线程获得了锁可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里)JVM通过account对象的Mark Word判断当前线程ID还在说明还持有着这个对象的锁就可以继续进入临界区工作。由于之前没有释放锁这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个很明显偏向锁几乎没有额外开销性能极高。
结论: JVM不用和操作系统协商设置Mutex(争取内核)它只需要记录下线程ID就标示自己获得了当前锁不用操作系统接入。
上述就是偏向锁在没有其他线程竞争的时候一直偏向偏心当前线程当前线程可以一直执行。
代码演示变化需使用jol-core演示版本如pom.xml
注意默认偏向锁程序启动4s后才会开启故程序编写睡眠了5s也可通过设置vm参数修改
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.example/groupIdartifactIdTestPlus/artifactIdversion1.0-SNAPSHOT/version/parentartifactIdTestMaven/artifactIdpropertiesmaven.compiler.source8/maven.compiler.sourcemaven.compiler.target8/maven.compiler.targetproject.build.sourceEncodingUTF-8/project.build.sourceEncoding/propertiesdependenciesdependencygroupIdorg.openjdk.jol/groupIdartifactIdjol-core/artifactIdversion0.9/version/dependency/dependencies/projectpublic class MainMaven {public final static int SIZE 10;public static void main(String[] args) throws InterruptedException {Object obj1 new Object();System.out.println(obj1加锁前 ClassLayout.parseInstance(obj1).toPrintable());TimeUnit.SECONDS.sleep(5);System.out.println(obj1程序启动5s后 ClassLayout.parseInstance(obj1).toPrintable());Object obj2 new Object();System.out.println(obj2加锁前 ClassLayout.parseInstance(obj2).toPrintable());synchronized (obj1){System.out.println(obj1加锁后 ClassLayout.parseInstance(obj1).toPrintable());}synchronized (obj2){System.out.println(obj2加锁后 ClassLayout.parseInstance(obj2).toPrintable());}}
}obj1加锁前java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes totalobj1程序启动5s后java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes totalobj2加锁前java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes totalobj1加锁后java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) b8 f2 cf 24 (10111000 11110010 11001111 00100100) (617607864)4 4 (object header) 7d 00 00 00 (01111101 00000000 00000000 00000000) (125)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes totalobj2加锁后java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 05 88 a8 71 (00000101 10001000 10101000 01110001) (1906870277)4 4 (object header) 92 01 00 00 (10010010 00000001 00000000 00000000) (402)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes total根据演示结果可推得结论
obj1加锁前和程序启动5s后偏向锁标志未变且obj2加锁前偏向锁开启-》偏向锁开启后创建的锁对象才具备偏向锁性质obj1加锁前锁标识为 001加锁后为00-》可由无锁状态直接升级为轻量锁obj2创建时锁标识即 101即偏向锁开启后创建的对象偏向锁位默认为1只是没有当前线程指针故其余比特为0
4.4 偏向锁的撤销
偏向锁使用一种等到竞争出现才释放锁的机制只有当其他线程竞争锁时持有偏向锁的原来线程才会被撤销。徽销需要等待全局安全点(该时间点上没有字节码正在执行)同时检查持有偏向锁的线程是否还在执行
第一个线程正在执行synchronized方法(处于同步块)它还没有执行完其它线程来抢夺该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有继续执行其同步代码而正在竞争的线程会进入自旋等待获得该轻量级锁第一个线程执行完成synchronized方法(退出同步块)则将对象头设置成无锁状态并撤销偏向锁重新偏向
4.5 题外话
因为维护成本高Java15逐步废弃偏向锁
5. 轻量级锁 轻量级锁多线程竞争但是任意时刻最多只有一个线程竞争即不存在锁竞争太过激烈的情况也就没有线程阻塞 主要作用轻量级锁是为了在线程近乎交替执行同步块时提高性能。有线程来参与锁的竞争但是获取锁的冲突事件极短本质就是自旋锁CAS 主要目的在没有多线程竞争的前提下通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗说白了先自旋升级时机当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁这时线程B又来抢该对象的锁由于该对象的锁已经被线程A拿到当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A)那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况
如果锁获取成功直接替换Mark Word中的线程ID为B自己的ID(A - B)重新偏向于其他线程(即将偏向锁交给其他线程相当于当前线程“被”“释放了锁)该锁会保持偏向锁状态A线程OverB线程上位;如果锁获取失败则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00)。此时轻量级锁由原持有偏向锁的线程持有继续执行其同步代码而正在竞争的线程B会进入自旋等待获得该轻量级锁。
5.1 轻量级锁的加锁
JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间官方称为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁会把锁的Mark Word复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的Mark Word替换为指向锁记录的指针。如果成功当前线程获得锁如果失败表示Mark Word已经被替换成了其他线程的锁记录说明在与其它线程竞争锁当前线程就尝试使用自旋来获取锁。
自旋CAS:不断尝试去获取锁能不升级就不往上捅尽量不要阻塞
5.2 轻量级锁的释放
在释放锁时当前线程会使用 CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁那么CAS操作会失败此时会释放锁并唤醒被阻寒的线程
5.3 锁升级
轻量锁自旋达到一定次数和程度时候就会升级到重量锁。
java6之前默认情况下自旋次数是10次或者自旋线程数超过CPU核数一半java6之后自适应自旋锁 原理线程如果自旋成功了那下次自旋的最大次数会增加因为JVM认为既然上次成功了那么这一次也很大概率会成功反之如果很少会自旋成功那么下次会减少自旋的次数甚至不自旋避免CPU空转根据同一个锁上一次自旋的时间和拥有锁线程的状态来决定最大的自旋的次数
6. 重量级锁
Java中synchronized的重量级锁是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始拉置插入monitor enter指令在结束位置插入monitor exit指令。 当线程执行到monitor enter指今时会尝试获取对象所对应的Monitor所有权如果获取到了即获取到了锁会在Monitor的owner中存放当前线程的id这样它将处于锁定状态除非退出同步块否则其他线程无法获取到这个Monitor
public class MainMaven {public final static int SIZE 10;private static Object lockObj;public static void main(String[] args) throws InterruptedException {lockObj new Object();System.out.println(lockObj加锁前 ClassLayout.parseInstance(lockObj).toPrintable());new Thread(() - {for (int i 0; i SIZE; i) {test();}}).start();new Thread(() - {for (int i 0; i SIZE; i) {test();}}).start();new Thread(() - {for (int i 0; i SIZE; i) {test();}}).start();}public static void test() {synchronized (lockObj) {System.out.println(lockObj加锁后 ClassLayout.parseInstance(lockObj).toPrintable());}}
}输出
lockObj加锁前java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes totallockObj加锁后java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 80 f3 8f ce (10000000 11110011 10001111 11001110) (-829426816)4 4 (object header) 1c 02 00 00 (00011100 00000010 00000000 00000000) (540)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes totallockObj加锁后java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 3a a0 c6 77 (00111010 10100000 11000110 01110111) (2009505850)4 4 (object header) 1c 02 00 00 (00011100 00000010 00000000 00000000) (540)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes total...lockObj加锁后java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 3a a0 c6 77 (00111010 10100000 11000110 01110111) (2009505850)4 4 (object header) 1c 02 00 00 (00011100 00000010 00000000 00000000) (540)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal 4 bytes external 4 bytes total可以看到从无锁-轻量级锁-重量级锁的过程
7. 锁升级与hashCode
锁升级为轻量级或重量级锁后Mark Word中保存的分别是线程栈里的锁记录指针和重量级锁指针已经没有位置再保存哈希码GC年龄了那么这些信息被移动到哪里去了呢?
在无锁状态下Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时JVM会生成对应的identity hash code值并将该值存储到Mark Word中对干偏向锁在线程获取偏向锁时会用Thread D和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后这个对象不能被设置偏向锁。因为如果可以的话那Mark Word中的identity hash code必然会被偏向线程ld给覆盖这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致 当一个对象已经计算过identity hash code它就无法进入偏向锁状态跳过偏向锁直接升级为轻量锁偏向锁过程中遇到一致性哈希计算请求立马撤销偏向模式膨胀为重量级锁 升级为轻量级锁时JVM会在当前线程的栈顺中创建一个锁记录(Lock Record)空间用于存储锁对象的Mark Word拷贝该拷贝中可以包含identity hash code所以轻量级锁可以和identity hash code共存哈希码和GC年龄自然保存在此释放锁后会将这些信息写回到对象头升级为重量级锁后Mark Word保存的重量级锁指针代表重量级锁的ObiectMonitor类里有字段记录非加锁状态下的Mark Word锁释放后也会将信息写回到对象头
默认情况下偏向锁开启后创建的对象的锁状态是010即偏向锁状态但是无线程指针此时若调用hashCode()方法对象会退回无锁状态001
8. 总结
锁优点缺点适用场景偏向锁加锁和解锁不需要额外的消耗和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景轻量级锁竞争的线程不会阻塞提高了程序的响应速度如果始终得不到锁竞争的线程适用自旋会消耗CPU追求响应速度同步块执行速度非常快重量级锁线程竞争不使用自旋不会消耗CPU线程阻塞响应时间缓慢追求吞吐量同步块执行速度较长
synchronized锁升级过程总结: 一句话就是先自旋不行再阻塞。实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式
synchronized在修饰方法和代码块在字节码上实现方式有很大差异但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁JDK1.6之后进行了优化拥有了无锁-偏向锁-轻量级锁-重量级锁的升级过程而不是无论什么情况都使用重量级锁
9. JIT编译器对锁的优化
JITJust In Time Compiler一般译为即时编译器
9.1 锁消除
逃逸分析-锁消除
sb是方法中的局部对象就只在该方法内的作用域有效不同线程调用该方法若使用锁同步机制则是白白浪费资源。故JIT在编译阶段会优化该处进行锁的消除
默认开启 -server -XX:DoEscapeAnalysis -XX:EliminateLocks 其中DoEscapeAnalysis表示开启逃逸分析EliminateLocks表示锁消除。
public class Main {public static void main(String[] args) {long start System.currentTimeMillis();int size 100000000;for (int i 0; i size; i) {createStringBuffer(JVM, 锁消除);}long timeCost System.currentTimeMillis() - start;System.out.println(createStringBuffer: timeCost ms);}public static String createStringBuffer(String str1, String str2) {StringBuffer sb new StringBuffer();sb.append(str1);// append方法是同步操作sb.append(str2);return sb.toString();}}默认启动
createStringBuffer:2377 ms关闭锁消除后
createStringBuffer:3933 ms9.2 锁粗化
假如方法中首尾相接前后相邻的都是同一锁对象那JIT编译器就会把这几个synchronized块合并为一个大块加粗加大范围一次申请锁即可避免次次的加锁和释放锁提升了性能
public class Main {public static void main(String[] args) {synchronized (Main.class) {System.out.println(1);}synchronized (Main.class) {System.out.println(2);}synchronized (Main.class) {System.out.println(3);}synchronized (Main.class) {System.out.println(1);System.out.println(2);System.out.println(3);}}
}