首页 > 其他 > 详细

深入分析CAS

时间:2020-07-24 10:19:50      阅读:68      评论:0      收藏:0      [点我收藏+]

在 Java 并发中,我们最初接触的应该就是 synchronized 关键字,但是 synchronized 属于重量级锁,很多时候会引起性能问题, volatile 也是个不错的选择,但是 volatile 不能保证原子性,只能在某些场合下使用。

 synchronized 这种独占锁属于悲观锁,它是在假设一定会发生冲突的,那么加锁恰好有用,除此之外,还有乐观锁,乐观锁的含义就是假设没有发生冲突,那么我正好可以进行某项操作,那么发生冲突呢,那我就重试直到成功,乐观锁最常见的就是 CAS 

1. 什么是 CAS

① CAS(compare and swap)比较并交换,比较和替换是线程并发算法时用到的一种技术

② CAS 是原子操作,保证并发安全,而不是保证并发同步

③ CAS 是 CPU 的一个指令

④ CAS 是非阻塞的、轻量级的乐观锁

2. 为什么说 CAS 是乐观锁

乐观锁,严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现等,所以 CAS 不会保证线程同步。

乐观的认为在数据更新期间没有其它线程影响。

3. CAS 原理

CAS(compare and swap)比较并替换,就是将内存值更新为需要的值,但是有个条件,内存值必须与期望值相同。

举个例子:期望值 E 、内存值 M 、更新值 U ,当 E == M 的时候将 M 更新为 U 。

4. CAS 应用 

由于 CAS 是 CPU 指令,我们只能通过 JNI 与操作系统交互,关于 CAS 的方法都在 sun.misc 包下 Unsafe 的类里 java.util.current.atomic 包下的原子类等通过 CAS 来实现原子操作。

6. CAS 举例

public class CasLock {

    private static CountDownLatch latch = new CountDownLatch(5); // 加法计数器
    private static int num = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        long time = System.currentTimeMillis();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int x = 0; x < 10000; x++) {
                    num++;                          //不是原子操作
                    atomicInteger.getAndIncrement();//调用原子类加1
                }
                latch.countDown(); // 线程执行完成,计数+1
            }).start();
        }
        latch.await();//保证所有子线程执行完成

        System.out.println("耗时: " + (System.currentTimeMillis() - time) + " 毫秒");
        System.out.println("num = " + num);
        System.out.println("atomicInteger = " + atomicInteger);
    }
}

输出结果:

耗时: 214 毫秒

num = 48450

atomicInteger = 50000

根据结果我们发现,由于多线程异步进行 num++ 操作,导致结果不正确。

为什么 num++ 的记过不正确呢?

比如:两个线程读到 num 的值为 1,然后做加 1 操作,这时候 num 的值是 2,而不是 3 而变量 atomicInteger 的结果却是对的,这就要归功于CAS,下面我们具体看一下原子类。

7. CAS指令和具体源代码

原子类例如 AtomicInteger 里的方法都很简单,大家看一看都能懂,我们具体看下 getAndIncrement 方法。

//该方法功能是Interger类型加1
public final int getAndIncrement() {
    //主要看这个getAndAddInt方法
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

//var1 是this指针
//var2 是地址偏移量
//var4 是自增的数值,是自增1还是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //获取内存值,这是内存值已经是旧的,假设我们称作期望值E
        var5 = this.getIntVolatile(var1, var2);
        //compareAndSwapInt方法是重点,
        //var5是期望值,var5 + var4是要更新的值
        //这个操作就是调用CAS的JNI,每个线程将自己内存里的内存值M
        //与var5期望值E作比较,如果相同将内存值M更新为var5 + var4,否则做自旋操作
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

getAndAddInt 方法的流程:假设有以下情景 ??

① A、B两个线程

② jvm 主内存的值1,A、B工作内存的值为 1(工作内存会拷贝一份主内存的值)

③ 当前期望值为 1,做加 1 操作

④ 此时 var5 = 1,var4 = 1,

    - 1. A 线程将 var5 与工作内存值 M 比较,比较 var5 是否等于 1

    - 2. 如果相同则将工作内存值修改为 var5 + var4 既修改为 2 并同步到主内存,此时 this 指针里,示例变量 value 的值就是 2 ,结束循环

    - 3. 如果不相同则其 B 线程修改了主内存的值,说明 B 线程已经先于 A 线程做了加 1 操作,A 线程没有更新成功需要继续循环,注意此时 var5 更新为新的内存值,假设当前的内存值是 2 ,那么此时 var5 = 2, var5 + var4 = 3 ,重复上述步骤直到成功

下面是compareAndSwapInt本地方法的源码,可以看到使用cmpxchg指令实现CAS,在效率上有不错的表现。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

8. CAS 优缺点

● 优点

非阻塞的轻量级的乐观锁,通过 CPS 指令实现,在资源竞争不激烈的情况下性能高,相比 synchronized 重量级锁,synchronized 会进行比较复杂的加锁,解锁和唤醒操作。

● 缺点

- 1. ABA问题:

CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A ,变成了 B ,又变成了 A ,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。

这就是 CAS 的 ABA 问题。 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A 。 目前在 JDK 的 atomic 包里提供了一个类 atomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

- 2. 循环时间长开销大:

例如 getAndAddInt 方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销

- 3. 只能保证一个共享变量的原子操作:

当对一个共享变量操作时,我们可以采用CAS的方式来保证原子操作。

但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性;

 

@ - 非原创 - 来源:掘金 - 转载自:zuckerbergJu2.0

深入分析CAS

原文:https://www.cnblogs.com/Dm920/p/13344329.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!