重学多线程(十)—— synchronized 原理与锁升级

时间:2021-01-01 13:01:28

前言

前几天去面试的时候,面试官问了一些关于 synchronized 关键字原理性的问题,回答得不甚理想。看来,平时对synchronized关键只能做到知其然,却不知其所以然,所以利用业余时间补习一下。

synchronized 原理

synchronized 关键字编译后会在同步块的前后添加上 montorenter 和 monitorexit 两个字节码指令,这两个字节码指令都需要一个指向锁定和解锁对象的 reference,如果指定了同步的对象reference就指向这个对象,如果修饰的是方法,如果是类方法就指向Class对象,如果是实例方法就指向这个实例。

对象头和锁

synchronized 使用的锁存在 Java 对象头中。HotSpot 虚拟机的对象头分两部分信息,第一部分用于存储对象自身的运行时数据,如HashCode,GC分代年龄等,这部分数据长度在32位和64位虚拟机中分别为32bit和64bit,它又称为“MarkWord”,它是实现锁的关键。另一部分就是用于存储指向方法区对象类型数据的指针,如果是数组的话,还有一个额外的空间储存数组长度。

对象头是与对象自己数据无关的额外储存成本,因此考虑到空间效率,MarkWord会根据自身的状态进行复用,也就是说在不同的状态下,它的储存结构不一样。在32位的HotSpot虚拟机中对象未锁定的状态下,Mark Word的32bit空间中的25bit用于储存对象的哈希码,4bit用于储存对象分代年龄,2bit用于储存锁标志位,1bit固定为0。

锁升级

在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

偏向锁

偏向锁的核心思想就是锁会偏向第一个获取它的线程,在接下来的执行过程中该锁没有其他的线程获取,则持有偏向锁的线程永远不需要再进行同步。

当一个线程访问同步块并获取锁的时候,会在对象头和栈帧中的锁记录里存储偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需要检查当前 Mark Word 中存储的线程是否为当前线程,如果是,则表示已经获得对象锁;否则,需要测试 Mark Word 中偏向锁的标志是否为1,如果没有则使用 CAS 操作竞争锁,如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

需要注意的是,偏向锁使用一种等待竞争出现才释放锁的机制,所以当有其他线程尝试获得锁时,才会释放锁。偏向锁的撤销,需要等到安全点。它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置为无锁状态;如果依然活动,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁或者标记对象不合适作为偏向锁(膨胀为轻量级锁),最后唤醒暂停的线程。

轻量级锁

线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于储存锁记录的空间(LockRecord),并将对象头的Mark Word信息复制到锁记录中。然后线程尝试使用 CAS 将对象头的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,并且对象的锁标志位转变为“00”,如果失败,表示其他线程竞争锁,当前线程便会尝试自旋获取锁。如果有两条以上的线程竞争同一个锁,那么轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态变为“10”,MarkWord中储存的就是指向重量级锁(互斥量)的指针,后面等待的线程也要进入阻塞状态。

轻量级锁解锁时,同样通过CAS操作将对象头换回来。如果成功,则表示没有竞争发生。如果失败,说明有其他线程尝试过获取该锁,锁同样会膨胀为重量级锁。在释放锁的同时,唤醒被挂起的线程。

重量级锁

重量级锁(Heavyweight Lock)是将程序运行交出控制权,将线程挂起,由操作系统来负责线程间的调度,负责线程的阻塞和执行。这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,消耗大量的系统资源,导致性能低下。

最后看一下,锁升级的图示过程:
重学多线程(十)—— synchronized 原理与锁升级