【JUC2022】第六章 Synchronized 与锁升级
文章目录
一、对象内存布局
二、锁升级
1.synchronized 锁优化的背景
用锁能够实现数据的安全性,但是会带来性能下降
无锁能够实现基于线程并行,提升程序性能,但是会降低安全性
因此,我们需要在二者之间寻求一个平衡
锁升级过程
无锁→偏向锁→轻量级锁→重量级锁
偏向锁:Mark Word 存储的是指向偏向的线程 ID
轻量级锁:Mark Word 存储的是指向线程栈中 Lock Record 的指针
重量级锁:Mark Word 存储的是指向堆中的 monitor 对象的指针
JDK1.5 及之前的 synchronized
Java 的线程是映射到操作系统原生线程之上的,如果要阻塞或者唤醒一个线程,就需要操作系统接入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自的内存空间和寄存器等。用户态切换至内核态需要传递很多变量和参数,内核态也需要保护好用户态在切换时的一些寄存器值和变量,以便内核态调用结束后切换回用户态继续工作
在 JDK1.5 及之前版本,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)依赖于操作系统的 Mutex Lock(系统互斥量)来实现的,挂起线程和恢复线程都需要切换到内核态,阻塞或者唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种切换需要耗费处理器时间
JDK1.6 为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁
2.无锁状态
Mark Word 偏向锁位为 0,锁标志位为 01
3.偏向锁
Mark Word 偏向锁位为 1,锁标志位为 01,并且有 54 位用于存放指向偏向的线程 ID
在大部分多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况。当同一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问时便会自动获得锁
4.轻量级锁
Mark Word 没有偏向锁位,锁标志位为 00,并且有 62 位用于存放指向线程栈中 Lock Record 的指针
当在偏向锁状态下,第二个线程并没有成功获取锁,那么就会升级为轻量级锁,此时轻量级锁由第一个线程继续持有。轻量级锁是为了在线程近乎交替执行同步代码块的时候提高性能,原理是通过 CAS 获取锁
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方称之为 Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其他线程竞争,当前线程就尝试使用自旋来获取锁
当自旋超过某个次数,就会升级为重量级锁,并且自旋的次数是自适应的。线程如果自旋成功了,那么下次自旋的最大次数会增加,因为 JVM 认为既然上次成功了,那么下次也很大概率会成功;繁殖,如果自旋失败,那么下次会减少自旋次数,避免 CPU 空转
5.重量级锁
Mark Word 没有偏向锁位,锁标志位为 10,并且有 62 位用于存放指向互斥量的指针
Java 中 synchronized 的重量级锁,是基于进入和退出 monitor 对象实现的。在编译时会将同步代码块的开始位置插入 monitor enter 指令,在结束位置插入 monitor exit 指令。当线程执行到 monitor enter 指令时,会尝试获取对象所对应的 Monitor 所有权,如果获取到了,即获取到了锁,会在 monitor 的 owner 中存放当前线程的 id,这样它将处于锁定状态,除非退出同步块,执行 monitor exit 命令,否则其他线程无法获取到这个 monitor
6.锁升级过程中 Mark Word 的变化
锁升级为轻量级锁或重量级锁后,Mark Word 中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码、GC 年龄了,那么这些信息去哪了?
当一个对象已经计算过一致性哈希码后,就再也无法进入偏向锁状态了,而是直接进入轻量级锁状态,将哈希码和 GC 年龄保存在线程栈帧的锁记录(Lock Record)中。当一个对象正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它会立即膨胀为重量级锁。在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的 ObjectMonitor 类里有字段可以记录非加锁状态下的 Mark Word,其中自然可以存储原来的哈希码
三、JIT 对锁的优化
JIT(Just In Time Compiler,即时编译器)
锁消除
如果使用的锁对象是每个线程都会 new 出来的,那么加的这把锁就毫无意义,因为线程根本不需要和其他线程竞争,因此 JIT 编译器会无视这个加锁
锁粗化
假如方法中前后相邻的同步代码块中用的都是同一个锁对象,那么 JIT 编译器就会把这个同步代码块合并成一个大块