Java多线程之CAS
CAS
介绍
独占锁是一种悲观锁,Synchronized就是一种悲观锁。会导致其他有需要锁的线程被挂起,等待持有锁的线程释放锁。更加有效的就是乐观锁。每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就进行重试,直到成功为止。乐观锁用到的机制就是CAS compare and swap
CAS的算法过程是这样, 它包含三个参数V, A, B. V表示内存值, A表示旧的预期的值, B表示要修改的新值. 仅当V值等于A值时,才会将V值设置为B, 如果V值和A值不同, 说明已经有其他线程做了更新,则当前线程什么都不做.当且仅当预期值A和内存值V相等时,将内存值修改为B并返回true,否则什么都不干并返回false。
简单的说,CAS需要额外给出一个期望值,也就是你认为这个变量现在应该是什么样子,如果变量不是你想象的那样,说明它已经被别人修改过了.
CAS中的比较和替换是一组原子操作,不会被外部打断,属于硬件级别的操作,效率要比加锁操作高。
所以就可以利用CPU的CAS指令,借助JNI来完成Java的非阻塞算法,其他原子操作都是利用类似的特性完成的。
实现分析
CAS中的比较和替换是一组原子操作,不会被外部打断,先根据paramLong/paramLong1获取到内存当中当前的内存值V,若有其他线程修改,则内存立马知道(设置可见),所以A的值就变了,就不能变成B了,要通过循环重新取内存中获取。 在将内存值V和原值A作比较,要是相等就修改为要修改的值B,属于硬件级别的操作,效率比加锁操作高
存在的问题
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
循环时间长开销大。自旋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操作。