CAS系列

一:理解Java并发里面的CAS概念

前言:
我们知道在Java多线程里面关于共享变量的操作,一定是要使用线程同步来保证线程安全的,一旦涉及线程同步,就需要加锁,一旦加锁就意味着某一个时候只能有一个线程在操作,其他的线程如果没有得到锁就会阻塞起来,此时的线程的状态是BLOCKED,当前面的线程释放锁的时候,系统会自动调度当前的线程进入临界区,这里面存在一个问题,就是线程的上下文切换的问题,虽然比起来进程的上下文切换,线程的上下文切换更轻量级,但仍然也是有一定开销的,比如最简单的i++的例子,那么如何有没有一种不需要加锁也能保证线程安全的数据结构呢?答案是肯定的,这就是今天需要谈到的CAS(Compare And Swap或 Compare And Set)。


什么是CAS:
CAS通常是指Compare And Swap或 Compare And Set,是硬件操作系统级别提供的具有原子性的原语指令(CAS的原子性是因为说cpu在执行cas时对这一块内存是独占排他的),利用它可以在多线程中取得和同步一样的效果。


CAS的原理:
CAS 算法大致原理是:在对变量进行计算之前(如 ++ 操作),首先读取原变量值,称为 旧的预期值 A,然后在更新之前再获取当前内存中的值,称为 当前内存值 V,如果 A==V 则说明变量从未被其他线程修改过,此时将会写入新值 B,如果 A!=V 则说明变量已经被其他线程修改过,当前线程应当什么也不做或者再循环几个周期直到更新成功。
CAS通过避免同步,也就是避免了线程的上下文来极大的提高了同步处理的能力,比如在多线程下使用AtomicLong的无锁累加要比,使用synchronized关键字处理性能高出很多。


CAS 缺点:
(1)ABA问题
通过上面的解释,我们知道CAS的原理是读取两个时刻的值,然后比较是否一致再决定是否更新,如果不一致,那么就需要多循环几次直到更新成功,这里面有一个问题假如第一次读到的预期值是A,然后在这段时间间隔内A的值变化成B又变化成A,这个时候其实它的值已经被更新过了,但是如果只比较值是没法判断出来是否更新过的,虽然对于一些计数,求和的操作不影响结果,但这是有缺陷的。 解决方法:通过版本号来判断,在每次更新后对应的版本号加1,这样一来在比较值之后如果相等,可以再次比较版本号来判断是否真的更新过。JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是 首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
(2)自旋多次循环导致的效率问题
上面说过CAS在判断两次读取的值不一样的时候会放弃操作,但为了保证结果正确,通常都会继续尝试循环再次发起CAS操作,如果连续多次CAS都失败,那么就会消耗大量的cpu资源。
(3)仅仅保证单个共享变量的原子操作?
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效;不过从 JDK 1.5开始提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作


CAS与Java并发工具包的关系:
java.util.concurrent包里面的工具类基本全部都是使用了CAS+volatile的乐观锁机制,也就是说Java并发工具包里面的大多数类底层是构建在CAS+volatile这种lock-free的机制上,所以拥有更好更高的并发效率。


总结:
CAS是一项基于乐观锁的非阻塞技术(不需要线程的上下文切换),虽然CAS也有自己的一些缺点,但其通过操作系统底层提供的原子指令来实现lock-free的取得同步效果,在大多数情况下其效率和性能都要比synchronized和lock更好。

 

二:CAS无锁原理简单理解

为什么要用CAS:
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。而且线程的数量是有限的,依赖于操作系统,而且线程的创建和销毁带来的性能损耗是不可以忽略掉的。虽然现在基本都是用线程池来尽可能的降低不断创建线程带来的性能损耗。

对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问时没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞。那多个线程共同访问临界区的资源怎么办呢,无锁的策略采用一种比较交换技术CAS(compare and swap)来鉴别线程冲突,一旦检测到冲突,就重试当前操作直到没有冲突为止。

与锁相比,CAS会使得程序设计比较复杂,但是由于其优越的性能优势,以及天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销,它比基于锁的方式有更优越的性能,所以在目前被广泛应用,我们在程序设计时也可以适当的使用。不过由于CAS编码确实稍微复杂,而且jdk作者本身也不希望你直接使用unsafe(后面会讲到)来进行代码的编写,所以如果不能深刻理解CAS以及unsafe还是要慎用,使用一些别人已经实现好的无锁类或者框架就好了。

CAS原理分析:

一个CAS方法包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。

额外引申技术点:volatile
上面说到当前线程可以发现其他线程对临界区数据的修改,这点可以使用volatile进行保证。
volatile实现了JMM中的可见性。使得对临界区资源的修改可以马上被其他线程看到,它是通过添加内存屏障实现的。

 

三:CAS无锁自旋和同步锁线程切换使用场景对比

1、对于资源竞争较少的情况,使用同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗CPU资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于锁。CAS在判断两次读取的值不一样的时候会放弃操作,但为了保证结果正确,通常都会继续尝试循环再次发起CAS操作,如Jdk1.6版本的AtomicInteger类的getAndIncrement()方法,就是利用了自旋实现多次尝试:

public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}

如果compareAndSet(current, next)方法成功执行,则直接返回;如果线程竞争激烈,导致compareAndSet(current, next)方法一直不能成功执行,则会一直循环等待,直到耗尽cpu分配给该线程的时间片,从而大幅降低效率。
总之:
1、使用CAS在线程冲突严重时,因为自旋会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用。
2、线程冲突严重的情况下,同步锁能实现线程堵塞和唤醒切换,不会出现自旋,避免了上述的情况,从而让性能远高于CAS。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值