Java 多线程与并发(四):CAS

解决线程安全问题,除了上面的 Synchronized 锁之外还有另外一个应用特别广泛的知识点 CAS,可以说 JUC 包完全是建立在 CAS 的基础之上的。

定义

CAS,compare and swap ,是计算机科学中一种实现多线程原子操作的指令,它比较内存中当前存在的值和外部给定的期望值,只有两者相等时,才将这个内存值修改为新的给定值。

CAS操作包含三个操作数,需要读写的内存位置(V)、拟比较的预期原值(A)和拟写入的新值(B),如果V的值和A的值匹配,则将V的值更新为B,否则不做任何操作。

如何解决线程安全问题

前几篇文章多次强调线程安全需要解决的三个问题,原子性,可见性和重排序问题,我们来看看 CAS 能不能解决。

原子性:除了上文介绍的监视器(monitor)的实现,CAS 也能实现读取和更新的原子性操作。

可见性:使用 volatile 关键字来保证。

重排序:使用 volatile 关键字来保证。

所以 CAS + volatile 也能够保证线程安全。

原理

我们拿 AtomicInteger 来看看 CAS 如何在无锁的条件下保证数据的正确性。

private volatile int value;

首先对于我们要操作的变量,需要使用 volatile 关键字,用来保证线程之间的可见性以及防止重排序。这样获取变量的值时就能够直接读取。

public final int get() {
        return value;
}

再来看 ++i 是如何做到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

每次从内存中读取数据,然后通过比较和更新,如果成功就返回,不成功就继续重试(for 循环符合 CAS 的原理,不断重试,直到成功为止)。

而 compareAndSet 利用 JNI 来完成 CPU 指令的操作。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

CAS 原理

我们想知道比较和更新这个本来应该是两步完成的操作是如何保证原子性的就需要研究 JNI 的代码了(unsafe 底层使用 C++ 实现)。

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);
 可以看到这是个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)。下面是对应于intel x86处理器的源代码的片段:

 

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

上面的代码可能很难看懂,其实我们知道一点就可以了,程序会根据当前处理器的类型来决定是否为 cmpxchg 指令添加 lock 前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略 lock 前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。如果这个操作给到了多个CPU,就破坏了原子性,所以多核环境肯定得先加一个 lock 指令,不管这个它是以总线锁还是以缓存锁来实现的。

Intel 对于 lock 前缀的说明如下:

确保对内存的读-改-写操作原子执行。

缺点

  1. ABA 问题,如果一个值从 A 变成了 B 又变成了 A,那么使用 CAS 检查时会发现它没有变化,但实际确实变化了。解决方法是使用版本号,每次更新时加上版本号。这样 A-B-A 就会变成 1A-2B-3A 了。
  2. 循环时间过长问题:如果 CAS 一直不成功,会给 CPU 带来很大的开销。如果 JVM 支持 pause 指令那么效率会有一定的提升。
  3. 只能保证一个共享变量的原子操作:如果又多个共享变量时,循环 CAS 就无法保证操作的原子性,这时候就必须用锁,也可以把多个变量合并成一个变量来操作,比如 i = 2, j = a 可以合并成 ij = 2a。JDK 1.5 提供了 AtomicReference 来保证引用对象的原子性。

Concurrent 包的实现

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

posted @ 2020-01-06 19:35  当年明月123  阅读(263)  评论(0编辑  收藏  举报