【并发编程】(十二)Java乐观锁的实现——CAS

1.CAS的概念

CAS的英文全称为CompareAndSwap,比较并替换的意思,通过这个名字可以大概猜到它的原理,在修改一个变量的时候需要用某种条件来做一个比较,如果比较成功则替换成新传入的值,如果比较失败就不执行更新操作。

CAS的设计思想是认为不会出现线程竞争,直接获取共享变量的值做修改,不需要在操作共享变量之前做阻塞的操作,整个行为表现的非常乐观,所有也被称为乐观锁

2.为什么要有CAS

在多线程环境下对共享变量的操作会导致线程安全问题,所以需要使用一种方式去限制多个线程对共享变量的操作,在之前的博客中提到了可以使用synchronized加上监视器锁来实现线程的互斥。
但是重量级锁的加锁和解锁涉及到线程的挂起和唤醒,这是个很消耗性能的操作,在每个线程持有锁的时间很短的情况下,可能会出现线程频繁的挂起和唤醒。

这时候就可以使用CAS作为替代方案:
CAS在对共享变量赋值时,会检查变量是否已经被其它线程修改了,如果没有则进行同步操作,如果被改了就不做操作。使用这种方式在存在线程竞争的情况下既不用阻塞线程,也可以保证线程安全。

既然CAS这么“完美”,为什么不都使用CAS来替代synchronized呢?下面可以看看CAS存在的缺点。

3.CAS的缺点

  • 自旋开销大
    CAS一般都是配合自旋来使用的,在CAS更新失败的时候通过自旋再次尝试去修改值,这种方式叫做自旋锁。自旋锁在大量并发的情况下,对共享变量的修改可能一直不成功,大量的线程就会一直在自旋,导致CPU空转严重,浪费大量的CPU资源。
  • 只能保证一个共享变量的原子操作
    当一个代码块中有多个对共享变量的操作,这时候使用CAS只能单独保证每一个共享变量的原子性,而不能保证整个代码块的操作是原子的。代码块的原子性还是得用synchronized或者Lock来保证。
  • ABA问题
    ABA问题是在说一个线程在给共享变量赋值的时候,去检查预期值的时候发现期望值没有被修改,但是这个预期值可能在事前被其它线程从A修改成了B,然后又修改回了A,这个是无法感知到的。但是大多数情况下,ABA问题并不影响程序运行的结果。

预期值的概念可以看下面的CAS原理。

4.CAS的原理

CAS的实现主要依赖于三个值:

  • 变量的内存偏移量offset
  • 旧的预期值expect
  • 待修改的新值update

在对共享变量进行操作时,先获取共享变量当前值作为expect,然后对这个值做修改得到一个修改值update,在对共享变量赋值的时候先通过offset获取到内存中的值,比如这个值与预期值except是否相等,如果相等则更新为新值update然后返回true,如果不相等则返回false。

在这里插入图片描述

5.CAS在Java中的应用

在Java中有一个sun.misc.Unsafe类,这个类中封装了一些native方法,CAS就是通过这个类中的方法来实现的。例如下面的几个方法:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

在JDK的源码中大量的使用了CAS作为并发编程中的原子性问题解决方案,例如ConcurrentHashMap、JUC中的原子类、Lock类等。在这里看一下原子类中的AtomicInteger是如何使用CAS保证操作的原子性的。

《多线程带来的线程安全问题》中的2.2原子性问题中有这么一个示例,使用多线程对共享变量Integer进行累加,因为value++不是一个原子性的操作,需要使用synchronized来保证线程安全。同样的,这里也可以使用CAS来保证原子性。看下面的示例:

public class AtomicDemo {

    static volatile AtomicInteger value = new AtomicInteger();

    public static void test() {
        value.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> AtomicDemo.test()).start();
        }
        Thread.sleep(1000L);
        System.out.println(value);
    }
}

不管怎么运行,打印的结果一定都是1000。实现原理可以看到incrementAndGet()方法:

public final int incrementAndGet() {
	// 此处传入内存偏移量和递增值1
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

然后继续往下看:

public final int getAndAddInt(Object var1, long valueOffset, int incr) {
	// 共享变量的当前值
    int expect;
    do {
    	// 每次进入循环都获取共享变量最新的当前值
        expect= this.getIntVolatile(var1, valueOffset);
    }
    // 1.将上面查询出的当前值作为期望值传入,当前值+1作为待更新的新值
    // 2.CAS判断是否更新成功,未成功则继续循环
    while(!this.compareAndSwapInt(var1, valueOffset, expect, expect + incr));

    return expect;
}

可以看到AtomicInteger就是通过CAS+自旋来实现对共享变量修改的原子性,在其他的源码中也大同小异,有兴趣可以去看看上面提到的那些源码。

6.总结

作用

  • 并发环境下保证对共享变量操作的原子性。

原理

  • 更新共享变量时,先判断共享变量是否已经被修改,只有在没有被修改的情况下,这次修改才会成功。
  • 共享变量当前值可以通过内存偏移量offset获取。
  • 可以通过当前值和期望值是否相等判断共享变量是否被修改。

缺点

  • 不适合大量线程竞争的场景。
  • 做不到对代码块的原子性保证。
  • ABA问题。

悲观锁与乐观锁

  • 悲观锁会认为一定会存在线程竞争,会在操作共享变量之前做锁的竞争,只有抢占到锁的线程才能操作共享变量。
  • 乐观锁认为不会存在线程竞争,所以不会有抢锁的操作,会先对共享变量做计算,在更新共享变量时再通过期望值和当前值是否相等去判断是否被别的线程修改过。

自旋锁

  • 通过循环执行CAS操作来保证对共享变量的修改成功。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挥之以墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值