在JDK 5以前Java语言是靠synchronized关键字保证同步的,这会致使有锁html
锁机制存在如下问题:java
(1)在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题。算法
(2)一个线程持有锁会致使其它全部须要此锁的线程挂起。数据库
(3)若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。服务器
volatile是不错的机制,可是volatile不能保证原子性。所以对于同步最终仍是要回到锁机制上来。数据结构
独占锁是一种悲观锁,synchronized就是一种独占锁,会致使其它全部须要锁的线程挂起,等待持有锁的线程释放锁。而另外一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。多线程
1、什么是CAS并发
CAS,compare and swap的缩写,中文翻译成比较并交换。异步
咱们都知道,在java语言以前,并发就已经普遍存在并在服务器领域获得了大量的应用。因此硬件厂商老早就在芯片中加入了大量直至并发操做的原语,从而在硬件层面提高效率。在intel的CPU中,使用cmpxchg指令。post
在Java发展初期,java语言是不可以利用硬件提供的这些便利来提高系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,于是java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。
CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。 若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。不然,处理器不作任何操做。不管哪一种状况,它都会在 CAS 指令以前返回该 位置的值。(在 CAS 的一些特殊状况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;若是包含该值,则将 B 放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可。”
一般将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来得到新 值 B,而后使用 CAS 将 V 的值从 A 改成 B。若是 V 处的值还没有同时更改,则 CAS 操做成功。
相似于 CAS 的指令容许算法执行读-修改-写操做,而无需惧怕其余线程同时 修改变量,由于若是其余线程修改变量,那么 CAS 会检测它(并失败),算法 能够对该操做从新计算。
2、CAS的目的
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。而整个J.U.C都是创建在CAS之上的,所以对于synchronized阻塞算法,J.U.C在性能上有了很大的提高。
3、CAS存在的问题
CAS虽然很高效的解决原子操做,可是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操做
1. ABA问题。由于CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,可是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html
2. 循环时间长开销大。自旋CAS若是长时间不成功,会给CPU带来很是大的执行开销。若是JVM能支持处理器提供的pause指令那么效率会有必定的提高,pause指令有两个做用,第一它能够延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它能够避免在退出循环的时候因内存顺序冲突(memory order violation)而引发CPU流水线被清空(CPU pipeline flush),从而提升CPU的执行效率。
3. 只能保证一个共享变量的原子操做。当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操做ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。
4、concurrent包的实现
因为java的CAS同时具备 volatile 读和volatile写的内存语义,所以Java线程之间的通讯如今有了下面四种方式:
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操做,这是在多处理器中实现同步的关键(从本质上来讲,可以支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,所以任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操做的原子指令)。同时,volatile变量的读/写和CAS能够实现线程之间的通讯。把这些特性整合在一块儿,就造成了整个concurrent包得以实现的基石。若是咱们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从总体来看,concurrent包的实现示意图以下:
1 package CASAtomic; 2 3 import java.util.concurrent.atomic.AtomicStampedReference; 4 5 public class AtomicStampedReferenceDemo { 6 7 static AtomicStampedReference<Integer> money=new AtomicStampedReference<Integer>(19,0); 8 9 public static void main(String[] args) { 10 //模拟多个线程同时更新后台数据库,为用户充钱 11 for(int i=0;i<3;i++){ 12 final int timestamp=money.getStamp(); 13 new Thread(){ 14 public void run() { 15 while(true){ 16 Integer m=money.getReference(); 17 if(m<20){ 18 if(money.compareAndSet(m, m+20, timestamp, timestamp+1)){ 19 System.out.println("余额小于20元,充值成功,余额:"+money.getReference()+"元"); 20 break; 21 } 22 }else{ 23 break; 24 } 25 } 26 }; 27 }.start(); 28 } 29 30 //用户消费线程,模拟消费行为 31 new Thread(){ 32 public void run() { 33 for(int i=0;i<100;i++){ 34 while(true){ 35 int timestamp=money.getStamp(); 36 Integer m=money.getReference(); 37 if(m>10){ 38 System.out.println("大于10元"); 39 if(money.compareAndSet(m, m-10, timestamp, timestamp+1)){ 40 System.out.println("成功消费10元,余额:"+money.getReference()); 41 break; 42 } 43 }else{ 44 System.out.println("没有足够的金额"); 45 break; 46 } 47 } 48 } 49 }; 50 }.start(); 51 } 52 53 }