关于java:一篇搞定CAS深度讲解面试实践必备

29次阅读

共计 6341 个字符,预计需要花费 16 分钟才能阅读完成。

背景

在高并发的业务场景下,线程平安问题是必须思考的,在 JDK5 之前,能够通过 synchronized 或 Lock 来保障同步,从而达到线程平安的目标。但 synchronized 或 Lock 计划属于互斥锁的计划,比拟重量级,加锁、开释锁都会引起性能损耗问题。

而在某些场景下,咱们是能够通过 JUC 提供的 CAS 机制实现无锁的解决方案,或者说是它基于相似于乐观锁的计划,来达到非阻塞同步的形式保障线程平安。

CAS 机制不仅是面试中会高频呈现的面试题,而且也是高并发实际中必须把握的知识点。如果你目前对 CAS 还不甚了解,或者只有含糊的印象,这篇文章肯定值得你花工夫学习一下。

什么是 CAS?

CASCompare And Swap 的缩写,直译就是 比拟并替换。CAS 是古代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种非凡指令,这个指令会对内存中的共享数据做原子的读写操作。其作用是让 CPU 比拟内存中某个值是否和预期的值雷同,如果雷同则将这个值更新为新值,不雷同则不做更新。

实质上来讲 CAS 是一种无锁的解决方案,也是一种基于乐观锁的操作,能够保障在多线程并发中保障共享资源的原子性操作,绝对于 synchronized 或 Lock 来说,是一种轻量级的实现计划。

Java 中大量应用了 CAS 机制来实现多线程下数据更新的原子化操作,比方 AtomicInteger、CurrentHashMap 当中都有 CAS 的利用。但 Java 中并没有间接实现 CAS,CAS 相干的实现是借助 C/C++ 调用 CPU 指令来实现的,效率很高,但 Java 代码需通过 JNI 能力调用。比方,Unsafe 类提供的 CAS 办法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。

CAS 的根本流程

上面咱们用一张图来理解一下 CAS 操作的根本流程。

在上图中波及到三个值的比拟和操作:批改之前获取的(待批改)值 A,业务逻辑计算的新值 B,以及待批改值对应的内存地位的 C。

整个解决流程中,假如内存中存在一个变量 i,它在内存中对应的值是 A(第一次读取),此时通过业务解决之后,要把它更新成 B,那么在更新之前会再读取一下 i 当初的值 C,如果在业务解决的过程中 i 的值并没有发生变化,也就是 A 和 C 雷同,才会把 i 更新(替换)为新值 B。如果 A 和 C 不雷同,那阐明在业务计算时,i 的值产生了变动,则不更新(替换)成 B。最初,CPU 会将旧的数值返回。而上述的一系列操作由 CPU 指令来保障是原子的。

在《Java 并发编程实际》中对 CAS 进行了更加艰深的形容:我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做批改,并通知我原来的值是多少。

在上述途程中,咱们能够很清晰的看到乐观锁的思路,而且这期间并没有应用到锁。因而,绝对于 synchronized 等乐观锁的实现,效率要高十分多。

基于 CAS 的 AtomicInteger 应用

对于 CAS 的实现,最经典最罕用的当属 AtomicInteger 了,咱们马上就来看一下 AtomicInteger 是如何利用 CAS 实现原子性操作的。为了造成更新显明的比照,先来看一下如果不应用 CAS 机制,想实现线程平安咱们通常如何解决。

在没有应用 CAS 机制时,为了保障线程平安,基于 synchronized 的实现如下:

public class ThreadSafeTest {

    public static volatile int i = 0;

    public synchronized void increase() {i++;}
}

至于下面的实例具体实现,这里不再开展,很多相干的文章专门进行解说,咱们只须要晓得为了保障 i ++ 的原子操作,在 increase 办法上应用了重量级的锁 synchronized,这会导致该办法的性能低下,所有调用该办法的操作都须要同步期待解决。

那么,如果采纳基于 CAS 实现的 AtomicInteger 类,上述办法的实现便变得简略且轻量级了:

public class ThreadSafeTest {private final AtomicInteger counter = new AtomicInteger(0);

    public int increase(){return counter.addAndGet(1);
    }

}

之所以能够如此平安、便捷的来实现平安操作,便是因为 AtomicInteger 类采纳了 CAS 机制。上面,咱们就来理解一下 AtomicInteger 的性能及源码实现。

CAS 的 AtomicInteger 类

AtomicInteger是 java.util.concurrent.atomic 包下的一个原子类,该包下还有 AtomicBoolean, AtomicLong,AtomicLongArray, AtomicReference 等原子类,次要用于在高并发环境下,保障线程平安。

AtomicInteger 罕用 API

AtomicInteger 类提供了如下常见的 API 性能:

public final int get():获取以后的值
public final int getAndSet(int newValue):获取以后的值,并设置新的值
public final int getAndIncrement():获取以后的值,并自增
public final int getAndDecrement():获取以后的值,并自减
public final int getAndAdd(int delta):获取以后的值,并加上预期的值
void lazySet(int newValue): 最终会设置成 newValue, 应用 lazySet 设置值后,可能导致其余线程在之后的一小段时间内还是能够读到旧的值。

上述办法中,getAndXXX 格局的办法都实现了原子操作。具体的应用办法参考下面的 addAndGet 案例即可。

AtomicInteger 外围源码

上面看一下 AtomicInteger 代码中的外围实现代码:

public class AtomicInteger extends Number implements java.io.Serializable {private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            // 用于获取 value 字段绝对以后对象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {throw new Error(ex); }
    }

    private volatile int value;

    // 返回以后值
    public final int get() {return value;}

    // 递减少 detla
    public final int getAndAdd(int delta) {
        // 1、this:以后的实例 
        // 2、valueOffset:value 实例变量的偏移量 
        // 3、delta:以后 value 要加上的数(value+delta)。return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    // 递减少 1
    public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
...
}

上述代码以 AtomicInteger#incrementAndGet 办法为例展现了 AtomicInteger 的根本实现。其中,在 static 动态代码块中,基于 Unsafe 类获取 value 字段绝对以后对象的“起始地址”的偏移量,用于后续 Unsafe 类的解决。

在解决自增的原子操作时,应用的是 Unsafe 类中的 getAndAddInt 办法,CAS 的实现便是由 Unsafe 类的该办法提供,从而保障自增操作的原子性。

同时,在 AtomicInteger 类中,能够看到 value 值通过 volatile 进行润饰,保障了该属性值的线程可见性。在多并发的状况下,一个线程的批改,能够保障到其余线程立马看到批改后的值。

通过源码能够看出,AtomicInteger 底层是通过 volatile 变量和 CAS 两者相结合来保障更新数据的原子性。其中对于 Unsafe 类对 CAS 的实现,咱们上面具体介绍。

CAS 的工作原理

CAS 的实现原理简略来说就是由 Unsafe 类 和其中的 自旋锁 来实现的,上面针对源代码来看一下这两块的内容。

UnSafe 类

在 AtomicInteger 外围源码中,曾经看到 CAS 的实现是通过 Unsafe 类来实现的,先来理解一下 Unsafe 类的作用。对于 Unsafe 类在之前的文章《各大框架都在应用的 Unsafe 类,到底有多神奇?》也有具体的介绍,大家能够参考,这里咱们再简略概述一下。

sun.misc.Unsafe 是 JDK 外部用的工具类。它通过裸露一些 Java 意义上说“不平安”的性能给 Java 层代码,来让 JDK 可能更多的应用 Java 代码来实现一些本来是平台相干的、须要应用 native 语言(例如 C 或 C ++)才能够实现的性能。该类不应该在 JDK 外围类库之外应用,这也是命名为 Unsafe(不平安)的起因。

JVM 的实现能够自由选择如何实现 Java 对象的“布局”,也就是在内存里 Java 对象的各个局部放在哪里,包含对象的实例字段和一些元数据之类。

Unsafe 里对于对象字段拜访的办法把对象布局形象进去,它提供了 objectFieldOffset()办法用于获取某个字段绝对 Java 对象的“起始地址”的偏移量,也提供了 getInt、getLong、getObject 之类的办法能够应用后面获取的偏移量来拜访某个 Java 对象的某个字段。在 AtomicInteger 的 static 代码块中便应用了 objectFieldOffset()办法。

Unsafe 类的性能次要分为内存操作、CAS、Class 相干、对象操作、数组相干、内存屏障、零碎相干、线程调度等性能。这里咱们只须要晓得其性能即可,不便了解 CAS 的实现,留神不倡议在日常开发中应用。

Unsafe 与 CAS

AtomicInteger 调用了 Unsafe#getAndAddInt 办法:

    public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

上述代码等于是 AtomicInteger 调用 UnSafe 类的 CAS 办法,JVM 帮咱们实现出汇编指令,从而实现原子操作。

在 Unsafe 中 getAndAddInt 办法实现如下:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

getAndAddInt 办法有三个参数:

  • 第一个参数示意以后对象,也就是 new 的那个 AtomicInteger 对象;
  • 第二个示意内存地址;
  • 第三个示意自增步调,在 AtomicInteger#incrementAndGet 中默认的自增步调是 1。

getAndAddInt 办法中,首先把以后对象主内存中的值赋给 val5,而后进入 while 循环。判断以后对象此刻主内存中的值是否等于 val5,如果是,就自增(替换值),否则持续循环,从新获取 val5 的值。

在上述逻辑中外围办法是 compareAndSwapInt 办法,它是一个 native 办法,这个办法汇编之后是 CPU 原语指令,原语指令是间断执行不会被打断的,所以能够保障原子性。

在 getAndAddInt 办法中还波及到一个实现 自旋锁。所谓的自旋,其实就是下面 getAndAddInt 办法中的 do while 循环操作。当预期值和主内存中的值不等时,就从新获取主内存中的值,这就是自旋。

这里咱们能够看到 CAS 实现的一个毛病:外部应用 自旋 的形式进行 CAS 更新(while 循环进行 CAS 更新,如果更新失败,则循环再次重试)。如果长时间都不胜利的话,就会造成 CPU 极大的开销。

另外,Unsafe 类还反对了其余的 CAS 办法,比方compareAndSwapObject compareAndSwapIntcompareAndSwapLong。更多对于 Unsafe 类的性能就不再开展,大家能够去看《各大框架都在应用的 Unsafe 类,到底有多神奇?》这篇文章。

CAS 的毛病

CAS高效的实现了原子性操作,但在以下三方面还存在着一些毛病:

  • 循环工夫长,开销大;
  • 只能保障一个共享变量的原子操作;
  • ABA 问题;

上面就这个三个问题具体讨论一下。

循环工夫长开销大

在剖析 Unsafe 源代码的时候咱们曾经提到,在 Unsafe 的实现中应用了自旋锁的机制。在该环节如果 CAS 操作失败,就须要循环进行 CAS 操作(do while 循环同时将期望值更新为最新的),如果长时间都不胜利的话,那么会造成 CPU 极大的开销。如果 JVM 能反对处理器提供的 pause 指令那么效率会有肯定的晋升。

只能保障一个共享变量的原子操作

在最后的实例中,能够看出是针对一个共享变量应用了 CAS 机制,能够保障原子性操作。但如果存在多个共享变量,或一整个代码块的逻辑须要保障线程平安,CAS 就无奈保障原子性操作了,此时就须要思考采纳加锁形式(乐观锁)保障原子性,或者有一个取巧的方法,把多个共享变量合并成一个共享变量进行 CAS 操作。

ABA 问题

尽管应用 CAS 能够实现非阻塞式的原子性操作,然而会产生 ABA 问题,ABA 问题呈现的根本流程:

  • 过程 P1 在共享变量中读到值为 A;
  • P1 被抢占了,过程 P2 执行;
  • P2 把共享变量里的值从 A 改成了 B,再改回到 A,此时被 P1 抢占;
  • P1 回来看到共享变量里的值没有被扭转,于是继续执行;

尽管 P1 认为变量值没有扭转,继续执行了,然而这个会引发一些潜在的问题。ABA 问题最容易产生在 lock free 的算法中的,CAS 首当其冲,因为 CAS 判断的是指针的地址。如果这个地址被重用了呢,问题就很大了(地址被重用是很常常产生的,一个内存调配后开释了,再调配,很有可能还是原来的地址)。

维基百科上给了一个形象的例子:你拿着一个装满钱的手提箱在飞机场,此时过去了一个火辣性感的美女,而后她很暖昧地撩拨着你,并趁你不留神,把用一个截然不同的手提箱和你那装满钱的箱子调了个包,而后就来到了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

ABA 问题的解决思路就是应用版本号:在变量后面追加上版本号,每次变量更新的时候把版本号加 1,那么 A ->B->A 就会变成 1A->2B->3A。

另外,从 Java 1.5 开始,JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 办法的作用是首先查看以后援用是否等于预期援用,并且查看以后标记是否等于预期标记,如果全副相等,则以原子形式将该援用和该标记的值设置为给定的更新值。

小结

本文从 CAS 的根本应用场景、根本流程、实现类 AtomicInteger 源码解析、CAS 的 Unsafe 实现解析、CAS 的毛病及解决方案等方面来全面理解了 CAS。通过这篇文章的学习想必你曾经更加粗浅的了解 CAS 机制了,如果对你有所帮忙,记得关注一下,继续输入干货内容。

正文完
 0