并发编程(五)之CAS乐观锁

概述

CAS:Compare and Swap,即比较再交换。

jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

什么是原子操作

假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么B全部执行完,要么完全不执行B,那么 A 和 B 对彼此来说是原子的。

如何实现原子操作

实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized 关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁。

这里会有些问题:

首先,如果被阻塞的线程优先级很高很重要怎么办?

其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。

还有一种情况,如果有大量的线程来竞争资源,那 CPU 将会花费大量的时间和资源来处理这些竞争,同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。

CAS算法理解

实现原子操作还可以使用当前的处理器基本都支持 CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个 CAS 操作过程都包含三个运算符:一个内存地址 V,一个期望的值 A 和一个新值 B,操作的时候如果这个地址上存放的值等于这个期望的值 A,则将地址上的值赋为新值 B,否则不做任何操作。CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环 CAS 就是在一个循环里不断的做 CAS 操作,直到成功为止。

CAS 是怎么实现线程的安全呢?语言层面不做处理,我们将其交给硬件—CPU 和内存,利用 CPU 的多处理能力,实现硬件层面的阻塞,再加上 volatile 变量的特性即可实现基于原子操作的线程安全。

CAS比较与交换的伪代码可以表示为:

do{

备份旧数据;

基于旧数据构造新数据;

}while(!CAS( 内存地址,备份的旧数据,新数据 ))

CAS实现原子操作的三大问题

ABA问题

因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行 检查时会发现它的值没有发生变化,但是实际上却变化了。 ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A→B→A 就会变成 1A→2B→3A。举个通俗点的 例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是 ABA 问题。

如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值 0,别人喝水前麻烦先做个累加才能喝水。

循环时间长开销大

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。

前面说过了,CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。但CAS就没有开销了吗?不!有cache miss的情况。这个问题比较复杂,首先需要了解CPU的硬件体系结构:

上图可以看到一个8核CPU计算机系统,每个CPU有cache(CPU内部的高速缓存,寄存器),管芯内还带有一个互联模块,使管芯内的两个核可以互相通信。在图中央的系统互联模块可以让四个管芯相互通信,并且将管芯与主存连接起来。数据以“缓存线”为单位在系统中传输,“缓存线”对应于内存中一个 2 的幂大小的字节块,大小通常为 32 到 256 字节之间。当 CPU 从内存中读取一个变量到它的寄存器中时,必须首先将包含了该变量的缓存线读取到 CPU 高速缓存。同样地,CPU 将寄存器中的一个值存储到内存时,不仅必须将包含了该值的缓存线读到 CPU 高速缓存,还必须确保没有其他 CPU 拥有该缓存线的拷贝。

比如,如果 CPU0 在对一个变量执行“比较并交换”(CAS)操作,而该变量所在的缓存线在 CPU7 的高速缓存中,就会发生以下经过简化的事件序列:

CPU0 检查本地高速缓存,没有找到缓存线。

请求被转发到 CPU0 和 CPU1 的互联模块,检查 CPU1 的本地高速缓存,没有找到缓存线。

请求被转发到系统互联模块,检查其他三个管芯,得知缓存线被 CPU6和 CPU7 所在的管芯持有。

请求被转发到 CPU6 和 CPU7 的互联模块,检查这两个 CPU 的高速缓存,在 CPU7 的高速缓存中找到缓存线。

CPU7 将缓存线发送给所属的互联模块,并且刷新自己高速缓存中的缓存线。

CPU6 和 CPU7 的互联模块将缓存线发送给系统互联模块。

系统互联模块将缓存线发送给 CPU0 和 CPU1 的互联模块。

CPU0 和 CPU1 的互联模块将缓存线发送给 CPU0 的高速缓存。

CPU0 现在可以对高速缓存中的变量执行 CAS 操作了

以上是刷新不同CPU缓存的开销。最好情况下的 CAS 操作消耗大概 40 纳秒,超过 60 个时钟周期。这里的“最好情况”是指对某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的CPU,所以对应的缓存线已经在 CPU 的高速缓存中了,类似地,最好情况下的锁操作(一个“round trip 对”包括获取锁和随后的释放锁)消耗超过 60 纳秒,超过 100 个时钟周期。这里的“最好情况”意味着用于表示锁的数据结构已经在获取和释放锁的 CPU 所属的高速缓存中了。锁操作比 CAS 操作更加耗时,是因深入理解并行编程

为锁操作的数据结构中需要两个原子操作。缓存未命中消耗大概 140 纳秒,超过 200 个时钟周期。需要在存储新值时查询变量的旧值的 CAS 操作,消耗大概 300 纳秒,超过 500 个时钟周期。想想这个,在执行一次 CAS 操作的时间里,CPU 可以执行 500 条普通指令。这表明了细粒度锁的局限性。

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。

Jdk中相关原子操作类

描述操作类
更新基本类型类AtomicBoolean,AtomicInteger,AtomicLong
更新数组类AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

更新基本类型类

AtomicInteger

方法描述
int addAndGet(int delta)以原子方式将输入的数值与实例中的值,(AtomicInteger 里的 value)相加,并返回结果
boolean compareAndSet(int expect,int update)如果输入的数值等于预期值,则以原子方式将该值设置为输入的值
int getAndIncrement()以原子方式将当前值加 1,注意,这里返回的是自增前的值
int getAndSet(int newValue)以原子方式设置为 newValue 的值,并返回
public class UseAtomicInt {
    static AtomicInteger ai = new AtomicInteger(10);

    public static void main(String[] args) {

        int addAndGet = ai.addAndGet(10);
        System.out.println("ai="+ai);
        System.out.println("addAndGet="+addAndGet);
        int getAndIncrement = ai.getAndIncrement();
        System.out.println("ai="+ai);
        System.out.println("getAndIncrement="+getAndIncrement);
    }
}

AtomicIntegerArray

主要是提供原子的方式更新数组里的整型,其常用方法如下

方法描述
int addAndGet(int i,int delta)以原子方式将输入值与数组中索引 i 的元素相加
boolean compareAndSet(int i,int expect,int update)如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置成 update 值

需要注意的是,数组 value 通过构造方法传递进去,然后 AtomicIntegerArray 会将当前数组复制一份,所以当 AtomicIntegerArray 对内部的数组元素进行修改时,不会影响传入的数组。

更新引用类型

原子更新基本类型的 AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic 包提供了以下 3个类。

  • AtomicReference

    原子更新引用类型

  • AtomicStampedReference

    利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在 ABA问题了。这就是 AtomicStampedReference 的解决方案。AtomicMarkableReference跟 AtomicStampedReference 差不多,AtomicStampedReference 是使用 pair 的 int stamp 作为计数器使用,AtomicMarkableReference 的 pair 使用的是 boolean mark。还是那个水的例子,AtomicStampedReference 可能关心的是动过几次, AtomicMarkableReference 关心的是有没有被人动过,方法都比较简单

  • AtomicMarkableReference

    原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是 AtomicMarkableReference(V initialRef,booleaninitialMark)

public class UseAtomicStampedReference {
    static AtomicStampedReference<String> asr
            = new AtomicStampedReference("mark",0);

    public static void main(String[] args) throws InterruptedException {
        //拿到当前的版本号(旧)
        final int oldStamp = asr.getStamp();
        final String oldReference = asr.getReference();
        System.out.println(oldReference+"============"+oldStamp);

        Thread rightStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":当前变量值:"
                        +oldReference + "-当前版本戳:" + oldStamp + "-"
                  + asr.compareAndSet(oldReference,
                        oldReference + "+Java", oldStamp,
                        oldStamp + 1));
            }
        });

        Thread errorStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                String reference = asr.getReference();
                System.out.println(Thread.currentThread().getName()
                        +":当前变量值:"
                        +reference + "-当前版本戳:" + asr.getStamp() + "-"
                        + asr.compareAndSet(reference,
                        reference + "+C", oldStamp,
                        oldStamp + 1));
            }
        });
        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();

        System.out.println(asr.getReference()+"============"+asr.getStamp());
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

汪了个王

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

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

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

打赏作者

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

抵扣说明:

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

余额充值