27.锁升级?synchronized 优化了解吗?
推荐阅读:偏向锁、轻量级锁、重量级锁到底是什么
什么是锁升级?
在 Java 对象头里,有一块结构,叫Mark Word,里面会记录锁的状态。
Mark Word 会记录对象自身的运行数据,如哈希码、GC 分代年龄、锁状态标志、偏向时间戳(Epoch) 等。
其中的锁状态标志位就是用来记录锁的状态的,有四种状态:
①、无锁状态,在这个状态下,没有线程试图获取锁。
②、偏向锁,当第一个线程访问同步块时,锁会进入偏向模式。
Mark Word 会被设置为偏向模式,并且存储了获取它的线程 ID。
偏向锁的目的是消除同一线程的后续锁获取和释放的开销。如果同一线程再次请求锁,就无需再次同步。
③、当有多个线程竞争锁,但没有锁竞争的强烈迹象(即线程交替执行同步块)时,偏向锁会升级为轻量级锁。
线程尝试通过CAS 操作(Compare-And-Swap)将对象头的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获取轻量级锁;如果失败,说明有竞争。
④、重量级锁,当锁竞争激烈时,轻量级锁会膨胀为重量级锁。
重量级锁通过将对象头的 Mark Word 指向监视器(Monitor)对象来实现,该对象包含了锁的持有者、锁的等待队列等信息。
synchronized 做了哪些优化?
在 JDK1.6 之前,synchronized 是直接调用 ObjectMonitor 的 enter 和 exit 实现的,这种锁也被称为重量级锁。这也是为什么很多声音说不要用 synchronized 的原因,有点“谈虎色变”的感觉。 从 JDK 1.6 开始,HotSpot 对 Java 中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,极大提升了 synchronized 的性能。 ①、偏向锁:当一个线程首次获得锁时,JVM 会将锁标记为偏向这个线程,将锁的标志位设置为偏向模式,并且在对象头中记录下该线程的 ID。 之后,当相同的线程再次请求这个锁时,就无需进行额外的同步。如果另一个线程尝试获取这个锁,偏向模式会被撤销,并且锁会升级为轻量级锁。 ②、轻量级锁:多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。 当一个线程尝试获取轻量级锁时,它会在自己的栈帧中创建一个锁记录(Lock Record),然后尝试使用 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针。 如果成功,该线程持有锁;如果失败,表示有其他线程竞争,锁会升级为重量级锁。 ③、自旋锁:当线程尝试获取轻量级锁失败时,它会进行自旋,即循环检查锁是否可用,以避免立即进入阻塞状态。 自旋的次数不是固定的,而是根据之前在同一个锁上的自旋时间和锁的状态动态调整的。 ④、锁粗化:如果 JVM 检测到一系列连续的锁操作实际上是在单一线程中完成的,则会将多个锁操作合并为一个更大范围的锁操作,这可以减少锁请求的次数。 锁粗化主要针对循环内连续加锁解锁的情况进行优化。 ⑤、锁消除:JVM 的即时编译器(JITopen in new window)可以在运行时进行代码分析,如果发现某些锁操作不可能被多个线程同时访问,那么这些锁操作就会被完全消除。锁消除可以减少不必要的同步开销。
锁升级的过程是什么样的?
无锁—>偏向锁---> 轻量级锁---->重量级锁。

大体上省简的升级过程是这样的:
完整的升级过程是这样的:
①、从无锁到偏向锁: 当一个线程首次访问同步块时,如果此对象无锁状态且偏向锁未被禁用,JVM 会将该对象头的锁标记改为偏向锁状态,并记录下当前线程的 ID。此时,对象头中的 Mark Word 中存储了持有偏向锁的线程 ID。 如果另一个线程尝试获取这个已被偏向的锁,JVM 会检查当前持有偏向锁的线程是否活跃。如果持有偏向锁的线程不活跃,则可以将锁重偏向至新的线程;如果持有偏向锁的线程还活跃,则需要撤销偏向锁,升级为轻量级锁。 ②、偏向锁的轻量级锁: 进行偏向锁撤销时,会遍历堆栈的所有锁记录,暂停拥有偏向锁的线程,并检查锁对象。如果这个过程中发现有其他线程试图获取这个锁,JVM 会撤销偏向锁,并将锁升级为轻量级锁。 当有两个或以上线程竞争同一个偏向锁时,偏向锁模式不再有效,此时偏向锁会被撤销,对象的锁状态会升级为轻量级锁。 ③、轻量级锁到重量级锁: 轻量级锁通过线程自旋来等待锁释放。如果自旋超过预定次数(自旋次数是可调的,并且自适应的),表明锁竞争激烈,轻量级锁的自旋已经不再高效。 当自旋等待失败,或者有线程在等待队列中等待相同的轻量级锁时,轻量级锁会升级为重量级锁。在这种情况下,JVM 会在操作系统层面创建一个互斥锁(Mutex),所有进一步尝试获取该锁的线程将会被阻塞,直到锁被释放。