乐趣区

Week-1-Java-多线程-CAS

前言

学习情况记录

  • 时间:week 1
  • SMART 子目标:Java 多线程

记录在学习线程安全知识点中,关于 CAS 的有关知识点。

线程安全是指:多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。

常见的线程安全实现方法分为不可变对象、线程互斥同步、非阻塞同步、线程本地存储等方案,本文要讲的就是非阻塞同步中的核心CAS.

非阻塞同步

从处理问题的方式上说,互斥同步属于一种悲观的并发策略。

随着 硬件指令集 的发展,我们可以采用 基于冲突检查的乐观并发策略,通俗地说,就是先行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现偶读不需要把线程挂起,因此这种同步操作称为非阻塞同步。

CAS

乐观锁需要 操作和冲突检测 这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成 硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

各种 Atomic 开头的原子类,内部都应用到了 CAS。就拿AtomicInteger 为例。

J.U.C 包里面的原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。

看看 AtomicInteger 对象一次自增,CAS起了什么作用,以下代码是 incrementAndGet() 的源码,可以看到内部调用了 Unsafe 对象的 getAndAddInt()

以下代码是 getAndAddInt()源码,var1 指示对象内存地址,var2指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较 如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4

compareAndSwapInt(var1, var2, var5, var5 + var4 其实换成 compareAndSwapInt(obj, offset, expect, update) 比较清楚,意思就是如果 obj 内的 valueexpect相等,就证明没有其他线程改变过这个变量,那么就更新它为 update,如果这一步的CAS 没有成功,那就采用自旋的方式继续进行 CAS 操作,取出乍一看这也是两个步骤了啊,其实在 JNI 里是借助于一个 CPU 指令完成的。所以还是原子操作。

CAS 的问题

  • ABA 问题

    • 描述:如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
    • 解决方案:J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果真的需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效
  • 循环时间长开销大

    • 自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来比较大的执行开销。
  • 只能保证一个共享变量的原子操作

    • CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作. 所以我们可以使用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作。

CAS 与 synchronized 的使用情景

  • 简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少)
  • synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)
  • 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  • 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

CAS 的应用

使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。关于非阻塞算法是属于 J.U.C 中并发容器部分的知识,属于比较难的内容。目前先引用几篇文章。作为记录。

  1. 非阻塞算法在并发容器中的实现
  2. 非阻塞同步算法实战(一)
  3. 非阻塞同步算法实战(二)-BoundlessCyclicBarrier
  4. 非阻塞同步算法实战(三)-LatestResultsProvider

参考

  1. 《深入理解 Java 虚拟机》
  2. https://www.ibm.com/developer…
退出移动版