深入讲解我们说的CAS自旋锁到底是什么
什么是自旋锁
说道自旋锁就要从多线程下的锁机制说起,由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutualexclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问。即每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁。
设计互斥算法时总是会面临一种情况,即没有获得锁的进程怎么办?
通常有2种处理方式:
一种是没有获得锁的调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这就是本文的重点——自旋锁。他不用将线城阻塞起来(NON-BLOCKING)。
另一种是没有获得锁的进程就阻塞(BLOCKING)自己,继续执行线程上的其他任务,这就是——互斥锁(包括内置锁Synchronized还有ReentrantLock等等)。
引言
CAS(Compareandswap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作。
它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回true。否则,返回false。
保证原子操作
任何技术的出现都是为了解决某些特定的问题,CAS要解决的问题就是保证原子操作。原子操作是什么,原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,知道操作完成。在多线程环境下,原子操作是保证线程安全的重要手段。举个例子来说,假设有两个线程在工作,都想对某个值做修改,就拿自增操作来说吧,要对一个整数i进行自增操作,需要基本的三个步骤:
1、读取i的当前值;
2、对i值进行加1操作;
3、将i值写回内存;
假设两个进程都读取了i的当前值,假设是0,这时候A线程对i加1了,B线程也加1,最后i的是1,而不是2。这就是因为自增操作不是原子操作,分成的这三个步骤可以被干扰。如下面这个例子,10个线程,每个线程都执行10000次i++操作,我们期望的值是100,000,但是很遗憾,结果总是小于100,000的。
staticinti=0; publicstaticvoidadd(){ i++; } privatestaticclassPlusimplementsRunnable{ @Override publicvoidrun(){ for(intk=0;k<10000;k++){ add(); } } } publicstaticvoidmain(String[]args)throwsInterruptedException{ Thread[]threads=newThread[10]; for(inti=0;i<10;i++){ threads[i]=newThread(newPlus()); threads[i].start(); } for(inti=0;i<10;i++){ threads[i].join(); } System.out.println(i); }
既然这样,那怎么办。没错,也许你已经想到了,可以加锁或者利用synchronized实现,例如,将add()方法修改为如下这样:
publicsynchronizedstaticvoidadd(){ i++; }
或者,加锁操作,例如下面使用ReentrantLock(可重入锁)实现。
privatestaticLocklock=newReentrantLock(); publicstaticvoidadd(){ lock.lock(); i++; lock.unlock(); }
CAS实现自旋锁
既然用锁或synchronized关键字可以实现原子操作,那么为什么还要用CAS呢,因为加锁或使用synchronized关键字带来的性能损耗较大,而用CAS可以实现乐观锁,它实际上是直接利用了CPU层面的指令,所以性能很高。
上面也说了,CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转,翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个CAS操作,当操作成功,返回true时,循环结束;当返回false时,接着执行循环,继续尝试CAS操作,直到返回true。
其实JDK中有好多地方用到了CAS,尤其是java.util.concurrent包下,比如CountDownLatch、Semaphore、ReentrantLock中,再比如java.util.concurrent.atomic包下,相信大家都用到过Atomic*,比如AtomicBoolean、AtomicInteger等。
这里拿AtomicBoolean来举个例子,因为它足够简单。
publicclassAtomicBooleanimplementsjava.io.Serializable{ privatestaticfinallongserialVersionUID=4654671469794556979L; //setuptouseUnsafe.compareAndSwapIntforupdates privatestaticfinalUnsafeunsafe=Unsafe.getUnsafe(); privatestaticfinallongvalueOffset; static{ try{ valueOffset=unsafe.objectFieldOffset (AtomicBoolean.class.getDeclaredField("value")); }catch(Exceptionex){thrownewError(ex);} } privatevolatileintvalue; publicfinalbooleanget(){ returnvalue!=0; } publicfinalbooleancompareAndSet(booleanexpect,booleanupdate){ inte=expect?1:0; intu=update?1:0; returnunsafe.compareAndSwapInt(this,valueOffset,e,u); } }
这是AtomicBoolean的部分代码,我们看到这里面又几个关键方法和属性。
1、使用了sun.misc.Unsafe对象,这个类提供了一系列直接操作内存对象的方法,只是在jdk内部使用,不建议开发者使用;
2、value表示实际值,可以看到get方法实际是根据value是否等于0来判断布尔值的,这里的value定义为volatile,因为volatile可以保证内存可见性,也就是value值只要发生变化,其他线程是马上可以看到变化后的值的;下一篇会讲一下volatile可见性问题,欢迎关注
3、valueOffset是value值的内存偏移量,用unsafe.objectFieldOffset方法获得,用作后面的compareAndSet方法;
4、compareAndSet方法,这就是实现CAS的核心方法了,在使用AtomicBoolean的这个方法时,只需要传递期望值和待更新的值即可,而它里面调用了unsafe.compareAndSwapInt(this,valueOffset,e,u)方法,它是个native方法,用c++实现,具体的代码就不贴了,总之是利用了CPU的cmpxchg指令完成比较并替换,当然根据具体的系统版本不同,实现起来也有所区别,感兴趣的可以自行搜一下相关文章。
使用场景
- CAS适合简单对象的操作,比如布尔值、整型值等;
- CAS适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致CPU开销很大;
比如AtomicBoolean可以用在这样一个场景下,系统需要根据一个布尔变量的状态属性来判断是否需要执行一些初始化操作,如果是多线程的环境下,避免多次重复执行,可以使用AtomicBoolean来实现,伪代码如下:
privatefinalstaticAtomicBooleanflag=newAtomicBoolean(); if(flag.compareAndSet(false,true)){ init(); }
比如AtomicInteger可以用在计数器中,多线程环境中,保证计数准确。
ABA问题
CAS存在一个问题,就是一个值从A变为B,又从B变回了A,这种情况下,CAS会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有AtomicStampedReference提供了根据版本号判断的实现,可以解决一部分问题。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。