Java synchronized锁升级过程和原理(新手必看)
从 Java 6 开始,synchronized 锁一共有 4 种状态,即无锁、偏向锁、轻量级锁和重量级锁,锁的状态会随着竞争情况逐渐升级。
synchronized 锁是由低(偏向锁)到高(重量级锁)进行升级的,可以升级但是不可以降级,其作用是提高获取锁和释放锁的效率。
synchronized 锁升级的原理与 Java 对象头有关。在 HotSpot 虚拟机中,Java 对象在内存中的结构大致可以分为对象头、实例数据和填充对齐三部分。由于 synchronized 锁是基于对象的,对象的锁信息又存储在对象头里,所以这里我们重点介绍一下对象头。
每个 Java 对象都有对象头:
在 32 位处理器中,一个字宽是 32 位;在 64 位虚拟机中,一个字宽是 64 位。一个数组类型的 Java 对象头结构如下表所示:
从上表可以看出,锁信息存储在对象头的 Mark Word 中,比如 Java 对象的线程锁状态及 GC 标记。线程锁状态就是前面提到的无锁、偏向锁、轻量级锁和重量级锁。
我们再看一下 Mark Word 的格式,如下表所示:
从上表可以看出:
至此,我们可以知道 synchronized 锁升级的原理其实是利用 Java 对象头中的 Mark Word 标记锁状态和线程 ID 等信息,在锁升级过程中,会更新 Mark Word 信息,详情如下。
1) 对象在没有被当成锁时,就是一个普通的对象,Mark Word 记录对象的 hashCode,锁标志位是 01,对应是否偏向锁的那一位是 0。
2) 当对象被当作同步锁并有一个线程 A 抢到了锁时,锁标志位还是 01,但对应是否偏向锁的那一位改成 1,前面 29 位或 61 位记录抢到锁的线程 ID、时间戳等,表示进入偏向锁状态。
3) 当线程 A 再次试图获得锁时,JVM 发现同步锁对象的标志位是 01,对应是否偏向锁的那一位是 1,也就是偏向状态,Mark Word 中记录的线程 ID 就是线程 A 自己的 ID,表示线程 A 已经获得了这个偏向锁,可以执行同步锁的代码。
4) 当线程 B 试图获得这个锁时,JVM 发现同步锁处于偏向状态,但是 Mark Word 中记录的线程 ID 不是线程 B 的 ID,那么线程 B 会先用 CAS 操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程 A 一般不会自动释放偏向锁。如果抢锁成功,就把 Mark Word 里的线程 ID 改为线程 B 的 ID,代表线程 B 获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行。
5) 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM 会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁 Mark Word 的指针,同时在对象锁 Mark Word 中保存指向这片空间的指针。上述两个保存操作都是 CAS 操作,如果保存成功,代表线程抢到了同步锁,就把 Mark Word 中的锁标志位改成 00,可以执行同步锁代码;如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤(6)。
6) 轻量级锁抢锁失败,JVM 会使用自旋锁。自旋锁不是一种锁状态,只代表不断地重试,尝试抢锁。从 Java 7 开始,自旋锁默认启用,自旋次数由 JVM 决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤(7)。
7) 在自旋锁重试之后,如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会被阻塞。
synchronized 锁是由低(偏向锁)到高(重量级锁)进行升级的,可以升级但是不可以降级,其作用是提高获取锁和释放锁的效率。
synchronized 锁升级的原理与 Java 对象头有关。在 HotSpot 虚拟机中,Java 对象在内存中的结构大致可以分为对象头、实例数据和填充对齐三部分。由于 synchronized 锁是基于对象的,对象的锁信息又存储在对象头里,所以这里我们重点介绍一下对象头。
每个 Java 对象都有对象头:
- 如果 Java 对象的类型是非数组类型,则用 2 个字宽来存储对象头,对象头则由 Mark Word 和 Class MetadataAddress 组成;
- 如果 Java 对象的类型是数组类型,则用 3 个字宽来存储对象头,对象头由 Mark Word、Class MetadataAddress 和 Array length 组成。
在 32 位处理器中,一个字宽是 32 位;在 64 位虚拟机中,一个字宽是 64 位。一个数组类型的 Java 对象头结构如下表所示:
表头1 | 表头2 | 表头3 |
---|---|---|
Mark Word | 存储对象的 hashCode、分代年龄和锁信息 | 32位/64位 |
Class Metadata Address | 存储对象类型数据的指针 | 32位/64位 |
Array length | 数组的长度 | 32位/64位 |
从上表可以看出,锁信息存储在对象头的 Mark Word 中,比如 Java 对象的线程锁状态及 GC 标记。线程锁状态就是前面提到的无锁、偏向锁、轻量级锁和重量级锁。
我们再看一下 Mark Word 的格式,如下表所示:
锁状态 | 29 位 或 61 位 | 1 位 是否为偏向锁 | 2位 锁标志位 |
---|---|---|---|
无锁 | 对象 hashCode、分代年龄 | 0 | 01 |
偏向锁 | 线程 ID、偏向锁时间戳、分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |
重量级锁 | 指向堆中Monitor对象的指针 | 10 | |
GC 标记 | GC 标记信息 | 11 |
从上表可以看出:
- 当对象状态为偏向锁时,Mark Word 存储的是偏向的线程 ID;
- 当对象状态为轻量级锁时,Mark Word 存储的是指向线程栈中 Lock Record 的指针;
- 当对象状态为重量级锁时,Mark Word 存储的是指向堆中的互斥锁 Monitor 对象的指针。
至此,我们可以知道 synchronized 锁升级的原理其实是利用 Java 对象头中的 Mark Word 标记锁状态和线程 ID 等信息,在锁升级过程中,会更新 Mark Word 信息,详情如下。
1) 对象在没有被当成锁时,就是一个普通的对象,Mark Word 记录对象的 hashCode,锁标志位是 01,对应是否偏向锁的那一位是 0。
2) 当对象被当作同步锁并有一个线程 A 抢到了锁时,锁标志位还是 01,但对应是否偏向锁的那一位改成 1,前面 29 位或 61 位记录抢到锁的线程 ID、时间戳等,表示进入偏向锁状态。
3) 当线程 A 再次试图获得锁时,JVM 发现同步锁对象的标志位是 01,对应是否偏向锁的那一位是 1,也就是偏向状态,Mark Word 中记录的线程 ID 就是线程 A 自己的 ID,表示线程 A 已经获得了这个偏向锁,可以执行同步锁的代码。
4) 当线程 B 试图获得这个锁时,JVM 发现同步锁处于偏向状态,但是 Mark Word 中记录的线程 ID 不是线程 B 的 ID,那么线程 B 会先用 CAS 操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程 A 一般不会自动释放偏向锁。如果抢锁成功,就把 Mark Word 里的线程 ID 改为线程 B 的 ID,代表线程 B 获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行。
5) 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM 会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁 Mark Word 的指针,同时在对象锁 Mark Word 中保存指向这片空间的指针。上述两个保存操作都是 CAS 操作,如果保存成功,代表线程抢到了同步锁,就把 Mark Word 中的锁标志位改成 00,可以执行同步锁代码;如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤(6)。
6) 轻量级锁抢锁失败,JVM 会使用自旋锁。自旋锁不是一种锁状态,只代表不断地重试,尝试抢锁。从 Java 7 开始,自旋锁默认启用,自旋次数由 JVM 决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤(7)。
7) 在自旋锁重试之后,如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会被阻塞。