(7)Java 中 synchronized 锁的理解,轻量级锁和重量级锁,自旋锁,对象 markword 的理解?

文章详细阐述了Java对象头的markword结构,包括无锁、轻量级锁和重量级锁的状态,以及锁升级的过程。在加锁后,对象的HashCode不会改变,因为加锁时会保存原始数据。偏向锁在JDK15后被弃用,轻量级锁状态下线程不会自旋,而是在升级为重量级锁后在等待集合中自旋。文章还介绍了synchronized的加锁、解锁和锁膨胀的流程,以及自旋加锁的原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

目录

1、对象头中的 markword 组成

2、有关偏向锁的理解

3、锁出现的情况

4、轻量级锁状态下,线程会自旋等待吗?

5、synchronized 加锁、解锁和锁升级(膨胀)的流程原理和源码分析?

5.1、加锁流程

5.2、解锁流程

5.3、锁膨胀流程:在锁膨胀的状态有四种

6、自旋加锁原理



1、对象头中的 markword 组成

在 64 bit 的虚拟机中,对象头中的 markword 格式

    
  * |-----------------------------------------------------------------|
  * |   unused |  hash  |  unused_gap  | age | biased_lock  |  lock   |
  * |     25   |   31   |     1        |  4  |      1       |   2     |
  * |-----------------------------------------------------------------| 

其中最后两位表示的就是当前对象的锁状态:01 表示无锁,00 表示轻量级锁、10 表示重锁。一旦对象加锁,markword 的格式就是分成了两部分

当对象开始处于无锁状态时,若 使用 synchronized 关键字加锁之后就会转变成轻量级锁,轻量级锁的 markword 的格式发生了变化。在轻锁的情况下 lock pointer 存储得就是获取了当前所的线程地址

 |--------------------------------|
 |      lock pointer     |   lock |
 |          62           |     2  |
 |--------------------------------|

当对象已经加锁(轻量级锁)时,若另一个线程又来抢占这把锁,就会出现锁升级的情况,升级成重量级锁 Monitor ,markword 格式和轻量级锁的格式相同, 只是在 lock pointer 指向的就是 Monitor 的内存地址。

提问:

我们知道,在原始 markword 中存储了当前对象的 hashCode 数据,当一旦加锁之后,会替换成加锁线程内存地址或者 Monitor 的内存地址,这样子原始的 markword 数据就丢失了,这时候若再次获取对象的 hashCode 是不是就和之前的不一样了呢?

但是进过验证会发现,同一个对象若经历几次加锁或者解锁,对象的 hashCode 是不会发生变化的,那是为什么呢?

前提知识:前提:我们都知道,Java 生成对象的 hashCode 的方式默认情况下是基于一种随机算法生成的,所以每一次启动获取的 hashCode 数据是不一样的。

解答:

在对象加轻量化级锁时,会将对象 markword 的前 60 和位数据同步存储到加锁线程对象的 markword 前60位中,当锁释放时, 再将线程中的 markword 前60位中的数据同步到加锁对象的 markword 中的相应的位置中去。

在加重量级锁时也是使用相同的原理, 但是在重量级释放锁时候情况是不一样的,Monitor 不会将对象头中的数据再次还原到加锁对象中去,而是后续加锁对象需要获取 hashCode 数据时根据自己对象头中的 Monitor 地址去 Monitor 对象的 markword 中获取。

2、有关偏向锁的理解

偏向锁在 JDK15 就开始弃用了,用要启用需要在启动项目是添加必要的虚拟机参数。后续可能会废弃偏向锁。

3、锁出现的情况

1:偏向锁:只有一个线程加锁的情况下,会出现偏向锁。此时一定没有竞争、没有竞争、没有竞争。

加锁之后 markword 存储的是线程的线程地址(操作系统线程),解锁的时候会将线程地址依然保留在 markword 中,目的是为了查看后续加锁线程是不是当前线程。当是同一个线程继续加锁是就不用再次加锁了,这样子可以提升效率。若不是同一个锁就会升级成轻量级锁。

2: 轻量级锁:当两个(或多个)线程对同一个对象加锁,但是是交替发生的,没有发生锁的竞争就会升级为轻量级锁。此时没有竞争。加锁之后 markword 存储java线程的线程地址。

3:重量级锁:当两个(或多个)线程对同一个锁出现了竞争就会升级为重量级锁,一定发生了锁的竞争。加锁之后 markword存储 Monitor 地址。

注意:锁的状态流向只能是低级到高级,不能反过来。重量级锁是不可能编程轻量级锁的。

4、轻量级锁状态下,线程会自旋等待吗?

有一个说法:在发生锁竞争时,轻量级锁状态下,再次想获取锁的线程会尝试自旋获取锁。如果在自旋的过程中对方解锁了,则自旋的这一方会获取锁进而避免阻塞。 如果竞争很激烈,自旋若干次之后都不能够获取锁,会逐渐膨胀成重量级锁,陷入阻塞。

这个说法是正确的吗?答案是是错误的。以下是正确说法:

在 Java 中,只要发生了锁的竞争,轻量级锁会立刻发生升级为 Monitor 重量级锁,在 Monitor 中有几个对象,其中有一个 owner 就是获取了当前所的线程。 还有一个等待集合 set,后续的线程来获取锁时,当锁被占用就会进入等待集合,刚刚进入等待集合的线程在这个时候会发生自旋等待,只要集合被唤醒就会去抢占锁。若等待集合长时间没有被唤醒,那个等待集合中的线程就会陷入阻塞状态。

所以说,轻量级锁状态线程是不会发生自旋的,在重量级锁的等待集合中开始会自旋等待。

5、synchronized 加锁、解锁和锁升级(膨胀)的流程原理和源码分析?

5.1、加锁流程

当一个线程为获取锁时,获取锁前锁对象有几种状态【考虑偏向锁】。

a:无锁,会添加偏向锁

b:有偏向锁 会判断是不是同一个线程加锁,若是,就维持偏向锁的状态,若不是(发生了锁竞争)偏向锁就会膨胀成为轻量级锁。

c:有轻量级锁,在获取锁时判断当前轻量级锁有没有被占用。 1、若轻量级锁没有被占用就直接获取锁,再次获取。 2、若轻量级锁被占用了,就会判断是不是相同的线程获取锁。 相同线程:发生了锁重入,重复获取锁时会在线程的 markword 中继续存储一个空指针,释放时可以直接释放。 不同线程:就发生了锁竞争,轻量级锁会马上升级成为重量级锁,当前线程进入等待集合自旋(或阻塞)。

d:有重量级锁 直接进入 Monitor 对象的等待集合自旋(或阻塞)。

若不考虑偏向锁,上面的流程中偏向锁就不存在了,会直接从无锁状态编程轻量级锁。

5.2、解锁流程

当离开 synchronized 关键字修饰的代码区域时就要解锁了【考虑偏向锁】。

在解锁时,当前锁的状态有偏向锁、轻量级锁和重量级锁 3 种状态。

a:偏向锁 添加偏向锁时,会将当前操作系统线程信息存储在对象的 markword 中,锁释放时 markword 中的线程信息会依然保留。

b:轻量级锁 在释放轻量级锁时,会获取锁对象中的 markword,当获取的是空指针时就说明发生了锁重入,直接释放即可,当获取了添加锁的 Java 线程对象时,就会发生原子替换,将锁 markword 中的相关信息恢复到线程对象中去,解锁成功。

c:重量级锁 按照重量级锁的解锁流程解锁。

5.3、锁膨胀流程:在锁膨胀的状态有四种

a:膨胀完毕: 只要锁对象的 markword 对象的后两位是 2,表示此时就是重量级锁,说明锁的膨胀过程已经结束,此时直接返回 Monitor 对象即可。

b:膨胀中: 当多个线程同时进入膨胀方法时,通过膨胀中的这个状态【markword 为0,不止后两位,全部为 0】,让一个线程执行膨胀,剩下的线程循环等待即可。

c:轻量级锁开始膨胀: 锁开始是轻量级锁,这时需要创建一个重量级锁对象 Monitor,用原子操作(cas)修改获取锁线程 markword。 若失败了,说明有其他线程在抢占锁,正在膨胀中,进入循环等待。 若成功了,操作流程是:

  1. 将锁对象的 markword 信息传递给 Monitor 对象。

  2. 设置 Monitor 的持锁线程。

  3. 将新的 Monitor 的对象的 markword 传递给获取锁线程.

  4. 当前线程的 markword 用于存放返回的 Monitor 对象的 markword。

d:无锁开始膨胀:

  1. 创建一个 Monitor 对象。

  2. 将锁对象原始的 markword 存入的 Monitor。

  3. 使用 CAS 操作设置将当前线程数据存放到 Monitor 对象,可能会成功或者失败。 成功:直接返回新的 Monitor 对象 markword。 失败:说明此时有其他线程在抢占当前的 Monitor 对象,循环等待即可。

6、自旋加锁原理

虽然自旋加锁常常被称为自旋锁,但是实际上它并不是一种独立机制的锁,而是重量级锁在陷入阻塞之前的一种尝试机制。

自旋的前提:只有在多处理器的情况下才会自旋。

自旋的两种方式:

a:固定自旋【jdk1.6以后就弃用了】 自旋 1000 次,每一个等待一段时间(大概几纳秒)

b:自适应自旋,有预自旋和自适应自旋两个阶段。 预自旋:会自旋 11 次,获取重量级锁成功就直接返回,失败了之后就进入完整的自旋流程。 完整自旋:会使用一个全局参数记录自旋次数,开始是 5000;每次自旋失败:下一次 -200,最少减少到 0。一次自旋成功,局参数记录自旋次数最小值为 1000,下一次 +100.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值