本篇文章整理书籍的第2章中有关synchronized锁的一些知识。在读部分前,作者知道哪些呢?
- synchronized加在实例方法上,锁的是this
- synchronized加在静态方法上,锁的是该类的字节码文件
- synchronized包裹代码块时,锁的是指定的对象
问题:
- 加锁的代码和不加锁的代码编译为字节码到底有何不同?
- 锁对象和锁字节码,到底是怎么锁法?
加锁后的字节码
这里我们分2种情况说,一种是代码块加锁。另一种是实例或静态方法加锁。
- 代码块方式加锁
- 被包裹的代码前加monitorenter
- 被包裹的代码后加monitorexit,这里的代码后,也包含异常退出时。说白了就是跳出这段包裹代码前,一定是走了monitorexit的。
- 实例或静态方法加锁
- 字节码对方法,标识上
ACC_SYNCHRONIZED
- 字节码对方法,标识上
对象头
如果想要搞清楚synchronized到底是怎么加锁的。那就必须先搞清楚,对象头。这里引出另一个问题。对象在内存中是如何存储的? 这里我也忘记了。但是知道在对象的内存存储结构中有一个对象头的一块区域。这一块区域就和我们的synchronized上锁、解锁操作有关。
字宽的概念 在32位和64位计算机中,1字宽的大小是不相等的。 32位中1字宽是 32bit 4字节 64位中1字宽是 64bit 8字节
对象头的结构
- 非数组类型的对象占2字宽(即没有 Array length 那项)
- 数组类型的对象 占3字宽(多一个数组长度)
Mark Word结构
- 32位
- 64位
加锁过程
说完了对象头中的Mark Word,下面我们就可以来看看各种锁的加锁过程
偏向锁的加锁过程
- 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个线程发现自己不是要锁的人时,会有两种情况:
- 第一个用锁的线程已经用完锁了,不存活了。
- 第一个用锁的线程还存活着。
对于第一种情况,会导致MarkWord重回无锁状态,之后线程2自然可以获取锁
对于第二种情况,会先暂停获取锁的这个线程,将MarkWord进行重新标记。之后继续运行这个线程。
个人理解,第二种情况,一般是需要一个锁升级的过程了。即修改MarkWord为 00 但是也不排除重新回到 01 无锁的状态
轻量级锁加锁过程
轻量级锁涉及到,栈帧中的LOCK RECORD记录拷贝MarkWord。每个需要锁的线程需要将MarkWord复制一份到LOCK RECORD中,之后再去CAS将MarkWord中的锁记录指针指向自己的栈帧。
解锁的话,释放锁的栈帧将当时保存的MarkWord副本替换回去,若成功,则解锁成功,不成功则引发锁膨胀
- 线程1 和 线程2 获取锁前都先复制MarkWord到自己栈帧中的Lock Record
- 线程1 CAS将MarkWord修改为自己LockRecord指针
- 线程2 自旋的修改MarkWord一直不成功后,直接将MarkWord修改为 10重量级锁,并将指针指向一个互斥重量级锁
- 线程1执行完后,先通过 CAS替换MarkWord,结果失败。
- 于是,线程1得知锁已升级,并释放锁,并唤醒其他陷入内核态等待的线程