CAS in java

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ziwang_/article/details/78529216

概念

CAS 英文全称 compare and swap,译为比较并交换,再通俗点 —— CAS 包含3个操作数,需要内存中的旧值 V,进行比较的值 A,以及拟写入的新值 B。当且仅当 V 等于 A 的时候,才会将 V 的值替换成 B,否则不执行任何操作。这里的“当且仅当”在高并发的情况下,可比高中数学中的“当且仅当”要难得多,所以后期就引入了 CAS 这么一个概念。这个概念看起来虽然通俗易懂,但是没有代码的演示还是不太行的,借用《Java Concurrency in Practice》 中的代码演示一下 ——

public class SimulatedCAS {
    private int value;

    public synchronized int get() {
        return value;
    }

    /**
     * 如果期望值与原值相等,那么将新值覆盖原值
     * @param expectedValue 期望值,用来与原值比较
     * @param newValue 新值,用来覆盖原值
     * @return 原值
     */
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue) {
            value = newValue;
        }

        return oldValue;
    }

    /**
     * 是否替换成功
     * @param expectedValue 期望值,用来与原值比较
     * @param newValue 新值,用来覆盖原值
     * @return true 表替换成功,false 表替换失败
     */
    public synchronized boolean compareAndSet(int expectedValue, int newValue) {
        return (expectedValue == compareAndSwap(expectedValue, newValue));
    }
}

ps:该代码仅表示语义,并不代表性能与具体实现。

比较

既然引入了这么一个机制,那么我们得思考下,java 中还有其它什么类似的机制用来解决类似的问题。实际上,CAS 常拿来与锁(即 synchorizedReentrantLock )做比较,也会拿来与 volitale 关键字做对比。

compare with volatile

volitale 修饰变量(后面暂述为目标变量)具有可见性是众所周知的,目标变量在线程中是共享。但是 java 中许多操作并不是原子操作,例如一下一行代码:

i++;

我们从视觉角度来说它就是一行代码,但是实际上细分下来它共有三个原子操作 ——

  • 读原 i 值
  • 计算 i + 1
  • 覆盖原 i 值

也就是读-改-写三个原子操作,但是三个原子操作合并起来就不是原子操作了(当然,这不能称为 volatile 的局限性,它的目的本不在此,本文重点基于 CAS,在此就不扩展)。那么如何解决类似这样的问题呢?一是锁,当然,这会在后面一节 compare with Lock 有所讨论,那么另一个解决方案就是 CAS 了,CAS 作为一种“更好的 volitale 变量”,不仅保证了可见性,又会保证原子性。

compare with Lock

事实上,加锁机制也是一种保证可见性和保证原子性的操作,所以 CAS 和锁机制时长拿来比较,例如 java 中的 ConcurrentHashMap 底层实现在 jdk1.7 时就是基于分段锁机制来保证线程安全的,而 jdk1.8 时使用 CAS (实际上还有 synchorized替代了大量的分段锁机制。

锁的劣势

我们知道,多个线程访问同一把锁的话,需要 CPU 来对线程进行调度,实际上,线程的挂起或恢复的过程中存在着很大的开销,那么如果实际上这个锁所修饰的内容是一个细粒度的操作(可以理解为简单操作),那么很有可能 CPU 的调度开销甚至会比执行锁中内容的操作开销大得多,这明显不是我们所期望的。

除此之外,我们知道,当一个线程在等待锁释放的过程中,是不能够做任何其它事情的,只能静静地阻塞,那么如果持有锁的线程被延迟执行或者永久被阻塞的话,其它线程只能一直等待下去,这明显不是我们所期望的,除前述的两个问题以外,还有一个优先级反转的问题 —— 假设当前被阻塞的线程优先级较高,而持有锁的线程优先级较低,那么即使搞高优先级线程拿到了可执行的权利,它也执行不了,从而导致了它的优先级往下减。

总结一下,锁在协调对共享状态访问情况下有以下缺点:

  • 对细粒度操作乏力,线程的调度开销可能大于工作开销
  • 线程阻塞
    • 持有锁线程因为异常而阻塞,将导致等待锁的线程也会被一直阻塞下去
    • 优先级反转问题

所以说,我们希望有一种新的技术 —— 它在管理线程之间竞争共享状态时开销比锁小,它类似于 volatile 变量,它还要支持原子的更新操作。

CAS 在 java 中的应用

前面我们提到 CAS 与 volatile、锁的比较,实际上并不是很严谨。实际上 java 是借助 CAS 封装了许多的类,而通过该些类来实现多线程中的共享变量安全访问,通过一个例子我们应该能够更好地理解 ——

public class CasCounter {
    // 一个模拟的 CAS 封装类
    private SimulatedCAS value;

    public int getValue() {
        // CAS 类含有一个 get() 返回当前值
        return value.get();
    }

    public int increment() {
        int v;

        // compareAndSwap() 可能返回的值有 v 或 v - 1 两种
        // 如果为 v - 1,那么继续重试
        // 如果为 v,则代表成功,停止重试
        do {
            v = value.get();
        } while (v != value.compareAndSwap(v, v + 1));

        return v + 1;
    }
}

CasCounter 类通过 SimulatedCAS(基于 CAS 封装的一个类)做出了一个非阻塞的计数器。这里也引入了 CAS 一个明显的缺点:需要自行处理竞争问题(通过重试、回退、放弃等,例如上例就是通过重试来解决竞争问题),因为它只会告诉你,当前获取的值是多少,当这个值不是你所期望的值的时候,需要做什么就是你自己来决定;但是锁不同,竞争锁阶段,未获取锁的线程都处于线程阻塞状态,而这一切都是 java 帮你做的。

当然,SimulatedCAS 是一个我们所创建的类,而不是 jdk 中的类,实际上,在 jdk1.5 之前,如果不编写明确的代码,那么就无法执行 CAS,而在 jdk1.5 之后 JVM 引入了底层支持,jdk 中也同时创建了12个基于 CAS 操作的原子类,在位于 /java/util/concurrent/atomic 包下:

这里写图片描述

由于笔者才学疏浅,只能就 AtomicInteger 这个类简单的介绍下,demo 如下:

public class Test {
    public static void main(String[] args) {
        AtomicInteger integer = new AtomicInteger();

        System.out.println("第一部分:");
        System.out.println(integer.get());
        integer.set(10);
        System.out.println(integer.get());

        System.out.println("第二部分:");
        System.out.println(integer.getAndIncrement());
        System.out.println(integer.get());

        System.out.println("第三部分:");
        System.out.println(integer.getAndSet(50));
        System.out.println(integer.get());

        System.out.println("第四部分:");
        System.out.println(integer.compareAndSet(10, 11));
        System.out.println(integer.get());

        System.out.println("第五部分:");
        System.out.println(integer.compareAndSet(50, 11));
        System.out.println(integer.get());
    }
}

打印如下:

第一部分:
0
10
第二部分:
10
11
第三部分:
11
50
第四部分:
false
50
第五部分:
true
11
  • AtomicInteger#get():获取当前值
  • AtomicInteger#set(int):放入指定值
  • AtomicInteger#getAndIncrement():将值加一,返回原值
  • AtomicInteger#compareAndSet(int, int):如果当前值为第一个参数,则返回 true,并将值改为第二个参数。否则返回 false,不改变原值

compareAndSet() 而言,我们来看看它的源码 ——

这里写图片描述

当然,再戳进去就是底层的 native 方法实现了 ——

这里写图片描述

在此就不做继续扩展了。实际上我们可以看到,关于 CAS 的所有底层操作都在这个 Unsafe 类中,并由它去调用相应的底层方法来实现 CAS 操作。

除了前面提到的缺点外,还有一个缺点就是著名的 ABA 问题 —— CAS 只会比较当前的 V 是否是我们所期望的 A,但是实际上 V 可能起初是 A,然后变成了 B 又变成了 A。但是可能在某种需求上,我们认为这样是发生了变化的,好在 jdk1.5 之后,存在一个叫做 AtomicStampedReference ,它可以维护一个对象引用-整型值的键值对,可以将整型值作为版本号来标记更新次数,用于满足我们的需求 ——

这里写图片描述

类似的还有一个 AtomicMarkableReference 的类维护的是 对象引用-布尔值 的键值对,在此笔者就不做扩展了。

猜你喜欢

转载自blog.csdn.net/ziwang_/article/details/78529216