Java中的乐观锁——无锁策略CAS算法

题主在阅读《实战Java高并发程序设计》一书时,了解到了Java无锁的相关概念,在此记录下来以加深对其的理解,Java中的锁分为两种即为悲观锁和乐观锁,那么何为悲观锁和乐观锁呢?

乐观锁与悲观锁

悲观锁是我们代码经常用到的,比如说Java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现,它总是假设别的线程在拿数据的时候都会修改数据,所以在每次拿到数据的时候都会上锁,这样别的线程想拿这个数据就会被阻塞直到它拿到锁。

乐观锁与之相反,它总是假设别的线程取数据的时候不会修改数据,所以不会上锁,但是会在更新的时候判断有没有更新过数据。也就是,乐观锁(无锁)使用一种比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突的产生,就重试当前操作直到没有冲突的产生。

与锁相比,使用比较交换(CAS)会使代码看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且线程之间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程之间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

乐观锁实现

乐视锁的实现之一就是CAS算法,CAS算法的过程大致是这样的:它包含三个参数CAS(V, E, N)。
需要读写的内存值: V,进行比较的预估值: A,拟写入的更新值: B。

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

仅当V等于E的时候,才会把V的值设置成N,否则不会执行任何操作(比较和替换是一个原子操作)。如果V值和E值不相等,则说明有其他线程修改过V值,当前线程什么都不做,最后返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功的完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会成功更新,其余都会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
在这里插入图片描述
实现过程
假如现在有两个线程t1,t2,,他们各自的运行环境中都有共享变量的副本V1、V2,预期值E1、E2,预期主存中的值还没有被改变,假设现在在并发环境,并且t1先拿到了执行权限,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试

然后t1比较预期值E1和主存中的V,发现E1=V,说明预期值是正确的,执行N1=V1+1,并将N1的值传入主存。这时候贮存中的V=21,然后t2又紧接着拿到了执行权,比较E2和主存V的值,由于V已经被t1改为21,所以E2!=V

t2线程将主存中已经改变的值更新到自己的副本中,再发起重试;直到预期值等于主存中的值,说明没有别的线程对旧值进行修改,继续执行代码,退出

乐观锁在JDK中的应用

在java.util.concurrent.atomic包下面的原子变量类就是使用了CAS来实现的,下面我们重点看一下CAS在该包下面的AtomicInteger类实际应用,该类提供下面几个核心方法和属性:

  • public final int incrementAndGet() // 当前值加1,返回旧值
  • public final int decrementAndGet() // 当前值减1,返回旧值
  • public volatile int value // AtomicInteger对象当前实际取值

incrementAndGet()和decrementAndGet()方法类似,我们只看一下incrementAndGet方法就好,JDK1.7与JDK1.8在实现incrementAndGet()方法有所区别(Java8中CAS的增强),下面给出的是在java8中的实现,可以看到incrementAndGet()实际调用的是sun.misc.Unsafe.getAndAddInt方法,Unsafe类可以理解为Java中指针,但是我们不可以直接使用,因为它是由Bootstrap类加载器加载,而非AppLoader加载。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;  // 
}

代码中的valueOffset代表value字段在AtomicInteger对象中的偏移量(到对象头部的偏移量),方便快速定位字段。

public final int getAndAddInt(Object obj, long l, int i)
{
    int j;
    do
        j = getIntVolatile(obj, l);
    while(!compareAndSwapInt(obj, l, j, j + i));
    return j;
}

传入getAndAddInt方法的参数分别是obj(AtomicInteger对象)、l(对象内偏移量)、i(增加值),可以看到getAndAddInt实际是一个循环,只有compareAndSwapInt返回true时,循环才能结束,并返回j(旧值),下面是compareAndSwapInt方法签名,其中前面两个参数和传入getAndAddInt方法参数一致,后面expected的值是通过getIntVolatile获取的旧值,x是希望设置的新值。

public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int x);

与compareAndSwapInt方法类似,getIntVolatile()内部也是用原子操作获取AtomicInteger对象的value值,下面是该方法的签名

public native int getIntVolatile(Object obj, long l);

CAS在JDK源码中应用广泛,下面给出其余的无锁的类:

  • AtomicReference 无锁的对象引用
  • AtomicStampedReference 带有标志的对象引用
  • AtomicIntegerArray 无锁的数组
  • AtomicIntegerFieldUpdater 无锁的普通变量

乐观锁的问题

  • ABA问题

如果一开始位置V得到的旧值是A,当进行赋值操作时再次读取发现仍然是A,并不能说明变量没有被其它线程改变过。有可能是其它线程将变量改为了B,后来又改回了A。大部分情况下ABA问题不会影响程序并发的正确性,如果要解决ABA问题,用传统的互斥同步可能比原子类更高效。
假设如下事件序列:

线程 1 从内存位置V中取出A。
线程 2 从位置V中取出A。
线程 2 进行了一些操作,将B写入位置V。
线程 2 将A再次写入位置V。
线程 1 进行CAS操作,发现位置V中仍然是A,操作成功

尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失

  • 解决方法一:

JDK 1.5以后提供了上文所说的AtomicStampedReference类来解决了这个问题,其中compareAndSet方法会首先检查当前引用是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值。

  • 解决方法二:

在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。

  • 自旋时间长

CAS自旋就是上文说的getAndAddInt()方法内部do-while循环,如果compareAndSwapInt一直未设置成功,do-while一直循环下去,会给CPU带来非常大的执行开销。网上给出执行方法如下,unchecked(还没试过~)

如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  • 只能保证单个共享变量

CAS操作只对单个共享变量有用,涉及多个变量时无法使用CAS,同样在JDK 1.5之后,提供了AtomicReference对象引用,可以多个变量放到一个AtomicReference对象里。

使用场景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)。

原文链接:https://blog.csdn.net/qq_28082579/article/details/84501257

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值