Java 中的乐观锁执行者:CAS

1、说明

        本篇博客旨在说明 CAS 的基础原理和简单操作,不会讲的太细。若想做更深入的研究,还请再参考下别的资料。若发现写的有什么问题,还请评论指出,谢谢。

2、原理

2.1 加锁和无锁

        在谈论无锁概念时,总会关联起乐观派与悲观派,对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事,但对于悲观派而言,他们总会认为发展事态如果不及时控制,以后就无法挽回了,即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁与无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为 CAS 的技术来保证线程执行的安全性,这项 CAS 技术就是无锁策略实现的关键,下面我们进一步了解 CAS 技术的奇妙之处。

2.2 无锁的执行者 - CAS

2.2.1 Java 中的锁

        首先,这里想简单说明一下,Java 中有两大锁:synchronized 和 lock,且他们都是悲观锁、重量级锁。

2.2.2 CAS

        CAS 的全称是 Compare And Swap 即比较交换,其执行函数为:CAS(V, E, N),其包含 3 个参数:

  • V 表示要更新的变量
  • E 表示预期值
  • N 表示新值

        如果 V 值等于 E 值,则将 V 的值设为 N。若 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是 CAS 操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行 CAS 操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下:
cas原理
        由于 CAS 操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS 操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁

2.2.3 为什么不会出问题?

        或许我们可能会有这样的疑问,假设存在多个线程执行 CAS 操作并且 CAS 的步骤很多,有没有可能在判断 V 和 E 相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为 CAS 是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致问题。

2.2.4 底层实现

        CAS 底层实现用到了 java 里面的 Unsafe 类,这个类里面的方法都用 native 修饰,即调用的是 c++ 的方法,属于系统原语。具体的方法我就不一一列举了,下面写出了几个主要的方法:

// 第一个参数 o 为给定对象,offset 为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
// expected 表示期望值,x 表示要设置的值,下面 3 个方法都通过 CAS 原子指令执行操作。

public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
 
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
 
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

2.2.5 Atomic 系列

        实际上我们在写代码的时候,用的是 java.util.concurrent.atomic 包下的 Atomic 系列,主要有以下 3 个:

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

2.2.6 CAS 的 ABA 问题及解决方案

        假设这样一种场景,当第一个线程执行 CAS(V, E, U)操作,在获取到当前变量 V,准备修改为新值 U 前,另外两个线程已连续修改了两次变量 V 的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图:
ABA问题
        这就是典型的 CAS 的 ABA 问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生 ABA 问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在 Java 中解决 ABA 问题,我们可以使用以下两个原子类:

  • AtomicStampedReference:带有时间戳的对象引用,在每次修改后,AtomicStampedReference 不仅会设置新值而且还会记录更改的时间。当 AtomicStampedReference 设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境;
  • AtomicMarkableReference:它不是维护时间戳,而是维护的是一个 boolean 值的标识,也就是 true 和 false 两种切换状态,这种方式并不能完全防止 ABA 问题的发生,只能减少 ABA 问题发生的概率。

3、实例

        首先我们来看一下多线程的异常问题:

public class AtomicBooleanTest implements Runnable {

    private final Test test = new Test();

    public static void main(String[] args) {
        AtomicBooleanTest abt = new AtomicBooleanTest();
        for (int i = 0; i < 10000; i++) {
            new Thread(abt).start();
        }
        try {
            // 这里 sleep 的原因是防止子线程未结束而主线程退出的问题
            Thread.sleep(2000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("i = " + abt.test.i);
    }

    @Override
    public void run() {
        try {
            // 这里 sleep 的原因是制造阻塞,来达到多线程乱序执行
            Thread.sleep(100L);
            test.i++;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    class Test {
        public int i = 0;
    }

}

执行结果:

i = 9851

不对呀,我们要得到 i = 10000,明显这就是多线程的问题

好了,我们通过加锁来解决问题:

@Override
public void run() {
    try {
        // 这里 sleep 的原因是制造阻塞,来达到多线程乱序执行
        Thread.sleep(100L);
        synchronized (test) {
            test.i++;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

输出为 i = 10000,问题解决了

最后,我不用锁,用 CAS 来解决问题:

public class AtomicBooleanTest implements Runnable {

    private final Test test = new Test();
    private static AtomicBoolean exits = new AtomicBoolean(true);

    public static void main(String[] args) {
        AtomicBooleanTest abt = new AtomicBooleanTest();
        for (int i = 0; i < 10000; i++) {
            new Thread(abt).start();
        }
        try {
            // 这里 sleep 的原因是防止子线程未结束而主线程退出的问题
            Thread.sleep(2000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("i = " + abt.test.i);
    }

    @Override
    public void run() {
        try {
            // 这里 sleep 的原因是制造阻塞,来达到多线程乱序执行
            Thread.sleep(100L);
            if (exits.compareAndSet(true, false)) {
                test.i++;
                exits.set(true);
            } else {
                run();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    class Test {
        public int i = 0;
    }

}

当第一个线程执行到 compareAndSet(期望值, 比对成功后重新赋值) 方法时,exits 默认值为 true,比较成功并将 exits 设置为 false,往下执行。此时当其余线程来的时候,会发现和期望的 true 不一致,就循环等待,直到可以执行。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值