编辑
2024-01-04
面试题库
0
请注意,本文编写于 384 天前,最后修改于 244 天前,其中某些信息可能已经过时。

在 JVM 的 synchronized 重量级锁涉及到操作系统(如 Linux)内核态下的互斥锁(Mutex)的使用,其线程阻塞和唤醒都涉及到进程在用户态和到内核态频繁切换,导致重量级锁开销大、性能低。而 JVM 的 synchronized 轻量级锁使用 CAS(Compare and Swap)进行自旋抢锁,CAS 是CPU 指令级的原子操作,并处于用户态下,所以 JVM 轻量级锁开销较小。

操作系统层面的 CAS 是一条 CPU 的原子指令(cmpxchg 指令),正是由于该指令具备了原子性,所以使用 CAS 操作数据时不会造成数据不一致问题,Unsafe 提供的 CAS 方法,直接通过native 方式(封装 C++代码)调用了底层的 CPU 指令 cmpxchg。

Unsafe 类是一个“final”修饰的不允许继承的最终类,而且其构造函数是 private 类型的方法,因此我们无法在外部对 Unsafe 进行实例化,那么怎么获取 Unsafe 的实例呢?可以通过反射的方式。

Unsafe 提供的 CAS 方法包含四个操作数——字段所处的对象、字段内存位置、预期原值及新值。在执行 Unsafe 的 CAS 方法的时候,这些方法首先将内存位置的值与预期值(旧的值)比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回 true;如果不相匹配,处理器不做任何操作,并返回 false。Unsafe 的 CAS 操作会将第一个参数(对象的指针、地址)与第二个参数(字段偏移量)组合在一起,计算出最终的内存操作地址。

CAS 是一种无锁算法,该算法关键依赖两个值——期望值(旧值)和新值,底层 CPU 利用原子操作,判断内存原值与期望值是否相等,如果相等则给内存地址赋新值,否则不做任何操作。使用 CAS 进行“无锁编程”(Lock Free)的步骤大致如下:

(1)获得字段的期望值(oldValue)。

(2)计算出需要替换的新值(newValue)。

(3)通过 CAS 将新值(newValue)放在字段的内存地址上,如果 CAS 失败则重复第 1 步到第 2 步,一直到 CAS 成功,这种重复俗称 CAS 自旋。

CAS中会遇到的ABA问题

什么是“ABA”问题?举一个例子来说明。比如说一个线程 A 从内存位置 M 中取出 V1,另一个线程 B 也取出 V1。现在假设线程 B 进行了一些操作之后将 M 位置的数据 V1 变成了 V2,然后又在一些操作之后将 V2 变成 V1。之后,线程 A 进行 CAS 操作,但是线程 A 发现 M 位置的数据仍然是 V1,然后线程 A 操作成功。尽管线程 A 的 CAS 操作成功,但是不代表这个过程是没有问题的,线程 A 操作的数据 V1 可能已经不是之前的 V1,而是被线程 B 替换过的 V1,这就是 ABA 问题。

很多乐观锁的实现版本,都是使用版本号(version)方式来解决 ABA 问题。乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

本文作者:whitebear

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!