返回介绍

6. 什么是 CAS

发布于 2024-09-08 13:17:47 字数 3522 浏览 0 评论 0 收藏 0

  • CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,Doug lea 大神在 java 同步器中大量使用了 CAS 技术,鬼斧神工的实现了多线程执行的安全性。
  • CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false。

一个 n++的问题

public class Case {
 
    public volatile int n;
 
    public void add() {
        n++;
    }
}

通过 javap -verbose Case 看看 add 方法的字节码指令

public void add();
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0       
         1: dup           
         2: getfield      #2                  // Field n:I
         5: iconst_1      
         6: iadd          
         7: putfield      #2                  // Field n:I

n++被拆分成了几个指令:

  • 执行 getfield 拿到原始 n;
  • 执行 iadd 进行加 1 操作;
  • 执行 putfield 写把累加后的值写回 n;

通过 volatile 修饰的变量可以保证线程之间的可见性,但并不能保证这 3 个指令的原子执行,在多线程并发执行下,无法做到线程安全,得到正确的 结果,那么应该如何解决呢?在 add 方法加上 synchronized 修饰解决,但是性能上差了点,除了低性能的加锁方案,我们还可以使用 JDK 自带的 CAS 方案,在 CAS 中,比较和替换是一组原子操作,不会被外部打断,且在性能上更占有优势。

在 JDK 5 之前 Java 语言是靠 synchronized 关键字保证同步的,这会导致有锁
锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile 是不错的机制,但是 volatile 不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁, synchronized 就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放 锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到 的机制就是 CAS,Compare and Swap。

并发之 CAS 操作

  • CAS 即 compare and set 的缩写。常见于 java.util.concurrent 中,是构成 concurrent 包的基础。
  • CAS 有三个操作数,内存值 M,旧的预期(expect) 值 E 和更新(update) 值 U。在 CAS 操作中,只有当 M==E 时,才会更新 U。否则什么都不做。这些操作都是原子的。

CAS 缺点

  • ABA 问题。 CAS 操作更新的基础是如果值没有变化则更新,若有变化则不更新。但如若有一值刚开始是 A,然后变为 B,最后又变为 A。那么 CAS 检查时发现它没有变化就更新了,但实际上却是已经发生了变化。
  • CAS 自旋。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性。

1.偏向锁

  • 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
  • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁
  • 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
  • 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正
    在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然 活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 MarkWord 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适 合作为偏向锁,最后唤醒暂停的线程。
  • 偏向锁在 Java 6 和 Java7 里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟:- XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM 参数关闭偏向 锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

2.轻量级锁

  • 线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 MarkWord 复制到锁记录中然后线程尝试使用 CAS 将对象头中的 MarkWord 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

  • 轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

3.锁的优缺点对比

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,适用自旋消耗 CPU追求响应时间,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗 CPU线程阻塞,响应时间缓慢追求吞吐量 同步块执行速度较长

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文