关于Java中锁的一些理解
Java 18

偏向锁—判定无资源竞争

首先对象头的mark word中有一个用于记录锁状态的段

有一个标志位用0/1表示无锁和偏向锁

如果在多线程情况下,一个锁第一次被一个线程访问,那么该资源会偏向这个线程

并且在接下来的运行过程没有线程访问这个锁

那么持有这个锁的线程就不需要触发同步

也就是**偏向锁在资源无竞争情况下消除了同步语句**,连 CAS 操作都不做了,着极大地提高了程序的运行性能。

既去掉了加锁释放锁的部分,这个标志位是1表示没有竞争,以后就不加锁了

实现流程

如果有资源竞争的情况

还是从第一次访问锁开始,首次访问对象头和栈帧中记录存储偏向锁的线程ID

如果下次进入检查这个ID位置是不是当前访问锁的线程

如果是,说明还是相同的线程获取,那么就不需要CAS和加锁这些繁琐的操作了

如果不是那就说明当前锁正在被多个线程竞争,就需要使用CAS替换线程ID为现在访问锁的新线程的ID

那么此时还会有两种情况,修改线程ID的操作可能成功也可能失败:

  • 成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;

  • 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

简而言之,就是看之前持有该锁的线程是否还占据着锁

这里为了方便理解举一个例子,加入有两个线程A、B竞争一个锁

  • 首先线程A访问锁,发现没有线程ID记录,于是JVM将该对象的Mark word中的线程ID改为线程A的ID并将是否是偏向锁的标志位设置为1

  • 然后线程B此时访问该对象中的同步块,检查对象头mark word发现线程ID是线程A的和线程B不符,所以 JVM 发现:**偏向线程与当前线程不一致,触发偏向撤销(revoke biased lock)**

这里修改线程的过程,JVM会使用CAS将线程ID进行修改,CAS的前提就是Mark word的线程ID仍然是线程A的,也就是比较之后发现符合预期才会修改

如果此时有第三个线程竞争锁,那么CAS会失效;

如果A仍然没释放锁同样也会失效,JVM会认为当前锁存在竞争

如果A已经释放锁了,并且CAS成功就会替换为线程B的ID

这样也就是说,如果B替换线程ID失败了就会触发锁升级,这样锁就会升级为轻量级锁

撤销偏向锁(偏向撤销)

上面说了,如果有竞争,就会触发锁升级,偏向锁是等到竞争出现才会释放的锁,有其它线程竞争偏向锁,持有锁的线程才会释放锁

如果发生了锁升级,那么会暂停拥有偏向锁的线程并且重置偏向锁标识:

1、在一个安全点(一个没有字节码执行的时间点)暂停拥有锁的线程(挂起)

2、遍历线程栈,如果存在琐记录需要修复让其编程无锁状态

3、唤醒被挂起的线程,将锁升级为轻量级锁

也就是说升级之后,锁仍然被之前的线程拥有,但是之后的竞争会采用轻量级锁的规则竞争

轻量级锁

轻量级锁真多的是多线程环境下,虽然多个线程需要获取同一把锁,但是获取时机是在不同的时间段,所以仍然不存在锁竞争,也就是没有发生阻塞,这种情况JVM采用轻量级锁

JVM会给每个线程的栈帧中创建一个空间,将锁的mark word复制过来

然后尝试用CAS替换mark word为指向栈中锁记录的指针。

如果替换成功,那么成功获取锁

如果失败,说明mark word已经被其它线程替换,当前存在竞争

线程会不断地尝试获取锁,一般采取循环实现(自旋)

当然自旋不是永无止境的,这个操作是十分消耗资源的,JVM中采用的是适应性的自旋,也就是如果自旋次数没超过阈值就获得了锁,那么下次这个阈值会提高,也就是允许自旋更多次数。

如果自旋超出阈值则认为失败,那么就认为当前线程阻塞,于是升级为重量级锁

轻量级锁的释放

当线程执行完同步块操作释放锁的时候,会把之前复制过来的mark word内容复制回mark word。如果没有竞争发生这个操作会成功过

如果其它线程自旋次数超出阈值失败了造成锁升级,这个CAS操作就会失败,此时会释放锁并唤醒被阻塞的线程(当前自旋失败线程)

这里引入一个案例方便理解:

🟢 起点:偏向锁

1. 线程 A 首次进入同步块

  • 锁对象处于无锁状态;

  • A 将**自己的线程 ID 写入对象头(Mark Word)**,对象变为**偏向锁状态**;

  • 加锁成功,进入临界区,**无加锁开销**。

🔵 线程 B 加入:触发偏向撤销

2. 此时线程 B 尝试进入相同的同步块

  • 发现锁对象已经**偏向于线程 A**;

  • JVM 尝试通过 CAS 撤销偏向锁(即尝试把线程 ID 改成自己的);

  • 由于 A 还未释放锁,**CAS 撤销失败 → 说明有竞争**;

  • JVM 判断出现竞争,**偏向锁失效**,**升级为轻量级锁**;

升级为轻量级锁

3. 升级为轻量级锁后的处理

  • JVM 会为**线程 A 和 B 各自在线程栈中创建锁记录(Displaced Mark Word)**;

  • A 如果还在运行同步块中,**继续执行,无需挂起**;

  • B 会尝试**CAS 替换对象头为指向自己锁记录的指针**;

  • 成功:B 获得锁(可能 A 已释放);

  • 失败:说明 A 仍持有锁,B 进入**自旋阶段**等待。


🔴 最终:可能升级为重量级锁

4. 如果 B 自旋多次仍失败

- VM 判断为**激烈竞争**,**锁升级为重量级锁(OS 层互斥量)**;

  • 此时**线程 B 被挂起**,等待 A 释放锁;

  • A 运行结束后**释放重量级锁**,唤醒 B。

如果A已经持有了锁则不会挂起,锁升级不影响线程的执行;

轻量锁升级会让竞争的锁被挂起而不是占据锁的线程

偏向锁下,线程 A 首次进入同步块并占据锁,对象头记录线程 ID。若线程 B 此时也尝试加锁,发现偏向锁已偏向 A,尝试 CAS 撤销失败,说明存在竞争。JVM 将锁升级为轻量级锁,A 不受影响继续执行,B 进入自旋等待获取锁。若自旋失败,锁再升级为重量级锁,B 被挂起等待 A 释放锁

轻量锁阶段的持锁线程是延续偏向锁阶段的线程,锁的所有权不会在升级过程中被重新竞争。除非 A 主动释放锁,否则 B 只能等着,或者自旋失败后被阻塞。

所以如果升级的时候没有释放A仍然占据着锁

重量级锁

重量级锁依赖的是操作系统的互斥锁

也就是同一时间只有一个线程可以执行某一段代码

但是转换成重量级锁的时间较长,不过被阻塞了的线程是不会消耗CPU的

对象锁会设置几种状态用来区分请求的线程:

  • Contention List:所有请求锁的线程将被首先放置到该竞争队列

  • Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry List

  • Wait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait Set

  • OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck

  • Owner:获得锁的线程称为 Owner

  • !Owner:释放锁的线程

如果线程尝试获取锁,但是此时锁被占用,那么就会将该对象封装后插入Contention List队头,然后挂起当前线程记录这个竞争锁的线程

当持有锁的线程释放锁,就会从Contention List和Entry List中挑选一个线程唤醒,这个被唤醒的线程会尝试获取锁,不过获取锁的概率并不是百分之百,因为重量级锁如果有线程获取失败会进入阻塞状态等待操作系统调度

如果线程获取锁后调用了wait方法(进入WAITING状态),那么释放锁并加入wait Set中,当被唤醒就加入Contention List 或 EntryList 中

用一个小故事来帮助理解:

类似于女神和备胎,女神同时只和一个男生处对象,但是当两人分手了,女神就会在自己的备胎清单(Contention List)里寻找下一个目标,同时这个清单中还有一些条件优秀的高级备胎作为预定的候选人(Entry List),之后会再这两个清单中挑选一名幸运成员。如果有人上位成功后有事情去外地出差了,则纳入(WaitSet)此时女神也不会等待直接挑选下一名备胎,然后当这个人回来了发现女神(被唤醒)已经和其他备胎好上了,于是一怒之下再次进入备胎列表(Contention List 或 EntryList 中去)竞争

总结—锁的升级流程

每一个线程在准备获取共享资源时: 第一步,检查 MarkWord 里面是不是放的自己的 ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。

第二步,如果 MarkWord 不是自己的 ThreadId,锁升级,这时候,用 CAS 来执行切换,新的线程根据 MarkWord 里面现有的 ThreadId,通知之前线程暂停,之前线程将 Markword 的内容置为空。(变为轻量锁)

第三步,两个线程都把锁对象的 HashCode 复制到自己新建的用于存储锁的记录空间,接着开始通过 CAS 操作, 把锁对象的 MarKword 的内容修改为自己新建的记录空间的地址的方式竞争 MarkWord。

第四步,第三步中成功执行 CAS 的获得资源,失败的则进入自旋 。

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。

第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

关于Java中锁的一些理解
https://talk2zbw.com/archives/JUC-01
作者
zbw
发布于
更新于
许可