一、什么是CAS?html
CAS:Compare and Swap,即比较再交换。java
jdk5增长了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5以前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。算法
二、CAS算法理解 数据库
对CAS的理解,CAS是一种无锁算法,CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。 编程
CAS比较与交换的伪代码能够表示为:数组
do{缓存
备份旧数据;安全
基于旧数据构造新数据;数据结构
}多线程
while(!CAS( 内存地址,备份的旧数据,新数据 ))
注:t1,t2线程是同时更新同一变量56的值
由于t1和t2线程都同时去访问同一变量56,因此他们会把主内存的值彻底拷贝一份到本身的工做内存空间,因此t1和t2线程的预期值都为56。
假设t1在与t2线程竞争中线程t1能去更新变量的值,而其余线程都失败。(失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次发起尝试)。t1线程去更新变量值改成57,而后写到内存中。此时对于t2来讲,内存值变为了57,与预期值56不一致,就操做失败了(想改的值再也不是原来的值)。
(上图通俗的解释是:CPU去更新一个值,但若是想改的值再也不是原来的值,操做就失败,由于很明显,有其它操做先改变了这个值。)
就是指当二者进行比较时,若是相等,则证实共享数据没有被修改,替换成新值,而后继续往下运行;若是不相等,说明共享数据已经被修改,放弃已经所作的操做,而后从新执行刚才的操做。容易看出 CAS 操做是基于共享数据不会被修改的假设,采用了相似于数据库的commit-retry 的模式。当同步冲突出现的机会不多时,这种假设能带来较大的性能提高。
原子是世界上的最小单位,具备不可分割性。好比 a=0;(a非long和double类型) 这个操做是不可分割的,那么咱们说这个操做时原子操做。再好比:a++; 这个操做实际是a = a + 1;是可分割的,因此他不是一个原子操做。非原子操做都会存在线程安全问题,须要咱们使用同步技术(sychronized)来让它变成一个原子操做。一个操做是原子操做,那么咱们称它具备原子性。
java的concurrent包下提供了一些原子类,咱们能够经过阅读API来了解这些原子类的用法。好比:AtomicInteger、AtomicLong、AtomicReference等。
除了在i++操做时使用synchroinzed关键字实现同步外,还可使用AtomicInteger原子类进行实现
Java.util.concurrent.atomic 包中提供了如下原子类, 它们是线程安全的类
Java提供的原子类是靠 sun 基于 CAS 实现的,CAS 是一种乐观锁。参考:乐观锁与悲观锁
原子变量类至关于一种泛化的 volatile 变量,可以支持原子的和有条件的读-改-写操做。AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(若是该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上很是像一个扩展的 Counter 类,但在发生竞争的状况下能提供更高的可伸缩性,由于它直接利用了硬件对并发的支持。
接下来经过源代码来看 AtomicInteger 具体是如何实现的原子操做。
首先看 value 的声明:
private volatile int value;
volatile 修饰的 value 变量,保证了变量的可见性。
incrementAndGet() 方法,下面是具体的代码:
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
经过源码,能够知道,这个方法的作法为先获取到当前的 value 属性值,而后将 value 加 1,赋值给一个局部的 next 变量,然而,这两步都是非线程安全的,可是内部有一个死循环,不断去作 compareAndSet 操做,直到成功为止,也就是修改的根本在 compareAndSet 方法里面,compareAndSet()方法的代码以下:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
compareAndSet()方法调用的compareAndSwapInt()方法的声明以下,是一个native方法。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, intvar5);
compareAndSet 传入的为执行方法时获取到的 value 属性值,next 为加 1 后的值, compareAndSet 所作的为调用 Sun 的 UnSafe 的 compareAndSwapInt 方法来完成,此方法为 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的。因此基于 CAS 的操做可认为是无阻塞的,一个线程的失败或挂起不会引发其它线程也失败或挂起。而且因为 CAS 操做是 CPU 原语,因此性能比较好。
相似的,还有 decrementAndGet() 方法。它和 incrementAndGet() 的区别是将 value 减 1,赋值给next 变量。
AtomicInteger 中还有 getAndIncrement() 和 getAndDecrement() 方法,他们的实现原理和上面的两个方法彻底相同,区别是返回值不一样,前两个方法返回的是改变以后的值,即 next。而这两个方法返回的是改变以前的值,即 current。还有不少的其余方法,就不列举了。
CAS(Compare-And-Swap)算法保证数据操做的原子性。
CAS 算法是硬件对于并发操做共享数据的支持。
CAS 包含了三个操做数:
内存值 V
预估值 A
更新值 B
当且仅当 V == A 时,V 将被赋值为 B,不然什么都不作,
固然若是须要的话,能够设计成自旋锁的模式,循环着不断进行判断 V 与 A 是否相等。
考虑以下问题:
关于CAS操做 提出问题:
状况1.
两个线程A和B同时对AtomicInteger(10)进行incrementAndGet()方法,都获取到current = 10 ,compareAndSet比较时,内存总的值均未被修改, 那两个线程都将执行了+1,那返回的结果应该都为11吧?
状况2
两个线程A和B同时对AtomicInteger(10)进行incrementAndGet()方法,都获取到current = 10 ,线程A线程先进行了compareAndSwapInt致使内存中的值变为11,那线程B的在和内存中的值比较一直不相等, 那线程B不是死循环了吗?
解决问题:
其实问题仍是在CAS上,内存值,预估值,更新值的问题
状况1:不会存在返回结果都是 11 的状况。原子类提供的就是原子操做,多线程状况下不会存在数据不一致的状况。具体缘由就是 CAS 操做,它会读取内存和预期值(11)做比较,若是相同才会进行赋值。
状况2:同理。
其实你说的两个线程“同时”,原子类的目的自己就是为了不这种场景下的数据不一致,因此你说的这两种状况是不存在的。
固然若是使用继承Thread类的方式实现多线程,那它的原子类变量是本身维护的,也就是线程独立的,那就会存在问题。实现Runnable接口就不会存在这个问题,由于是资源共享的。
三、CAS开销
前面说过了,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 条普通指令。这代表了细粒度锁的局限性。
如下是cache miss cas 和lock的性能对比:
四、CAS算法在JDK中的应用
在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操做,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。
Java 1.7中AtomicInteger.incrementAndGet()的实现源码为:
因而可知,AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了类sun.misc.Unsafe库里面的 CAS算法,用CPU指令来实现无锁自增。因此,AtomicInteger.incrementAndGet的自增比用synchronized的锁效率倍增。
参考: java中的原子类