本篇文章整理书籍的第2章中有关synchronized锁的一些知识。在读部分前,作者知道哪些呢?

  1. synchronized加在实例方法上,锁的是this
  2. synchronized加在静态方法上,锁的是该类的字节码文件
  3. synchronized包裹代码块时,锁的是指定的对象

问题:

  1. 加锁的代码和不加锁的代码编译为字节码到底有何不同?
  2. 锁对象和锁字节码,到底是怎么锁法?

加锁后的字节码

这里我们分2种情况说,一种是代码块加锁。另一种是实例或静态方法加锁。

  1. 代码块方式加锁
    • 被包裹的代码前加monitorenter
    • 被包裹的代码后加monitorexit,这里的代码后,也包含异常退出时。说白了就是跳出这段包裹代码前,一定是走了monitorexit的。
  2. 实例或静态方法加锁
    • 字节码对方法,标识上ACC_SYNCHRONIZED

对象头

如果想要搞清楚synchronized到底是怎么加锁的。那就必须先搞清楚,对象头。这里引出另一个问题。对象在内存中是如何存储的? 这里我也忘记了。但是知道在对象的内存存储结构中有一个对象头的一块区域。这一块区域就和我们的synchronized上锁、解锁操作有关。

字宽的概念 在32位和64位计算机中,1字宽的大小是不相等的。 32位中1字宽是 32bit 4字节 64位中1字宽是 64bit 8字节

对象头的结构

image.png

  • 非数组类型的对象占2字宽(即没有 Array length 那项)
  • 数组类型的对象 占3字宽(多一个数组长度)

Mark Word结构

  • 32位

image.png

  • 64位

image.png

加锁过程

说完了对象头中的Mark Word,下面我们就可以来看看各种锁的加锁过程

偏向锁的加锁过程

image.png

  • MarkWord中是无锁状态标识,锁状态:01 ,是否偏向锁:0
  • 线程1 进入去MarkWord中检查是否存储了自己的线程ID号
  • 线程1发现存储了自己的线程ID,直接继续运行锁内代码
  • 线程1发现没有存储自己的线程ID,且非偏向锁状态。就使用CAS方式将自己的线程ID记录到MarkWord以及是否偏向锁状态:1
  • 接着线程1运行自己锁内的代码
  • 与此同时,线程2也过来要锁了
  • 线程2进入MarkWord中检查是否存储了自己线程ID号
  • 线程2发现并不是自己的线程ID(此时是线程1的线程ID)
  • 线程2 继续判断MarkWord是不是偏向锁状态
  • 若不是偏向锁,证明它是第一个进入这个锁的线程。于是乎可以记录自己线程ID,设置为偏向锁状态
  • 若是偏向锁,证明它是第2个进入这个锁的线程,会走一个偏向锁撤销的逻辑

    偏向锁撤销逻辑

    撤销偏向锁的原因是当第2个线程发现自己不是要锁的人时,会有两种情况:
  1. 第一个用锁的线程已经用完锁了,不存活了。
  2. 第一个用锁的线程还存活着。

对于第一种情况,会导致MarkWord重回无锁状态,之后线程2自然可以获取锁
对于第二种情况,会先暂停获取锁的这个线程,将MarkWord进行重新标记。之后继续运行这个线程。

个人理解,第二种情况,一般是需要一个锁升级的过程了。即修改MarkWord为 00 但是也不排除重新回到 01 无锁的状态

轻量级锁加锁过程

轻量级锁涉及到,栈帧中的LOCK RECORD记录拷贝MarkWord。每个需要锁的线程需要将MarkWord复制一份到LOCK RECORD中,之后再去CAS将MarkWord中的锁记录指针指向自己的栈帧。
解锁的话,释放锁的栈帧将当时保存的MarkWord副本替换回去,若成功,则解锁成功,不成功则引发锁膨胀
image.png

  • 线程1 和 线程2 获取锁前都先复制MarkWord到自己栈帧中的Lock Record
  • 线程1 CAS将MarkWord修改为自己LockRecord指针
  • 线程2 自旋的修改MarkWord一直不成功后,直接将MarkWord修改为 10重量级锁,并将指针指向一个互斥重量级锁
  • 线程1执行完后,先通过 CAS替换MarkWord,结果失败。
  • 于是,线程1得知锁已升级,并释放锁,并唤醒其他陷入内核态等待的线程

参考资料