序言
本文给大家介绍一下 synchronized 关键字的部分原理。
一、内存中的 Java 对象
class A {
private String attr;
}
先引入一个问题:上面类 A 有一个属性 attr。当类 A 实例化之后的对象在内存中是如何表示的呢?
在内存中,Java 对象由三部分组成。其中包括对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)等部分。
- 对象头(Object Header):对象头存储了对象的元数据信息,比如哈希码、锁状态、垃圾回收标记等。对象头的大小在 32 位和 64 位的 JVM 上可能会有所不同,通常占用 8 字节或更多的空间。
- 实例数据(Instance Data):实例数据是对象的成员变量(字段)的实际存储区域。每个字段根据其类型占用不同的内存空间。例如,一个 int 类型的字段占用 4 字节,一个对象引用占用 4 字节(在 32 位 JVM 上)或 8 字节(在 64 位 JVM 上)。
- 对齐填充(Padding):由于硬件要求数据在内存中的地址是对齐的,因此在实例数据和对象头之间可能存在一些填充字节,以保证对象的起始地址是对齐的。填充的大小通常是对象头和实例数据大小的倍数,以满足对齐要求。
二、Java 对象头
对象头包含了两部分,分别是运行时元数据(Mark Word)和类型指针(Klass Word)。
在 32 位的虚拟机中,对象头占 64 位。其中 Mark Word 占 32 位,Klass Word 占 32 位。
在 64 位的虚拟机中,对象头占 128 位。其中 Mark Word 占 64 位,Klass Word 占 64 位。
接下来我们以 64 位的虚拟机为例。
三、Mark Word
对象头的 Mark Word 存储了对象的元信息,其中包括了对象的锁状态、GC(垃圾回收)相关信息等。 Mark Word 中通常用来表示对象的锁状态的部分称为锁标志位 (Lock Word)
,它包含了对象的锁状态、线程 ID 等信息。
通常情况下,锁标志位可以有以下几种状态:
- 无锁状态:对象尚未被锁定,可以被任意线程访问。
- 偏向锁状态:对象已经被某个线程锁定,但是尚未涉及竞争。在这种状态下,MarkWord 中会记录拥有锁的线程 ID,并且对象的锁标志位中会设置偏向锁标志。
- 轻量级锁状态:多个线程竞争同一个锁,但尚未涉及到真正的阻塞,因此采用了一种轻量级的锁机制来进行竞争。在这种状态下,MarkWord 中会记录锁的指针,用于指向锁记录(Lock Record),并且对象的锁标志位中会设置轻量级锁标志。
- 重量级锁状态:当轻量级锁竞争不过,会升级为重量级锁,这时会涉及到阻塞和唤醒线程的操作。在这种状态下,MarkWord 中不再存储锁记录的指针,而是直接指向锁对象,并且对象的锁标志位中会设置重量级锁标志。
- GC 标志位:有些 JVM 实现中,MarkWord 还可能包含 GC 相关的标志位,用于标记对象是否被回收等信息。
3.1 无锁状态的 Mark Word
上图是 64 位虚拟机中无锁
状态的 Mark Word。表示无锁是 Mark Word 的后三位(Mark Word 后三位 001
表示无锁),即:baised_lock 标志位是 0,Mark Word 最后两位是 01。
3.2 偏向锁状态的 Mark Word
上图是 64 位虚拟机中偏向锁
状态的 Mark Word。表示偏向锁是 Mark Word 的后三位(后三位 101
表示偏向锁),即:baised_lock 标志位是 1,Mark Word 最后两位是 01。
3.3 轻量级锁状态的 Mark Word
上图是 64 位虚拟机中轻量级锁
状态的 Mark Word。后两位 00
表示轻量级锁。
3.4 重量级锁状态的 Mark Word
上图是 64 位虚拟机中重量级锁
状态的 Mark Word。后两位 10
表示轻量级锁。
3.5 GC 标志位的 Mark Word
上图是 64 位虚拟机中 GC 标志位
的 Mark Word。后两位 11
表示 GC 标志位。
四、synchronized 工作流程
class A {
private String attr;
public void setAttr(String attr) {
// 使用 synchronized 加锁
synchronized (this) {
this.attr = attr;
}
}
}
在上述代码中,setAttr() 方法中使用了 synchronized,其中锁住的是当前对象 A。
- 当使用 synchronized 锁住对象 A ,对象 A 未被使用时,对象 A 的 Mark Word 依旧没有改变(即 Mark Word 后三位是
001
,无锁状态)。 - 当第一次有一个线程去访问对象 A 时,此时 Mark Word 的锁标志位会变成
101
(即表示对象现在使用的偏向锁)。 - 若后面有少量其他线程也去获取对象 A 的锁,对象 A 会先撤销偏向锁,偏向锁撤销成功则尝试采用
CAS
加上轻量级锁,轻量级锁加锁成功则将 Mark Word 的锁标志位变成00
(即表示对象现在使用的轻量级锁) - 若后面有更多的线程前来争抢对象 A 的锁,其他未抢到锁的线程就会发生
CAS 自旋
。自旋超时之后,系统便判断当前对于该锁的竞争非常激烈,将会撤销轻量级锁,然后加上重量级锁,并将锁标志位相应地更新为重量级锁即10
通过上面的分析,我们知道 synchronized 在底层存在一种锁升级机制
而不是使用一种固定的锁。这种锁升级机制是根据不同的并发度使用不同的加锁方式(这也是 Java 团队对 synchronized 的优化)。
当我们使用 synchronized 关键字时,一般就认为该部分代码会在多线程环境中执行。如果对象从无锁状态一步一步升级会浪费性能。所以,通常 JVM 会开启偏向锁,即对象创建之后 Mark Word 的后三位是 101 而不是 001。
往期推荐
- 为什么 MySQL 单表数据量最好别超过 2000w
- ConcurrentHashMap 源码分析(一)
- IoC 思想简单而深邃
- ThreadLocal
- JDK 动态代理