Java乐观锁实现之CAS操作
介绍cas操作前,我们先简单看一下乐观锁 与 悲观锁这两个常见的锁概念。
悲观锁:
从java多线程角度,存在着“可见性、原子性、有序性”三个问题,悲观锁就是假设在实际情况中存在着多线程对同一共享的竞争,所以在操作前先占有共享资源(悲观态度)。因此,悲观锁是阻塞,独占的,存在着频繁的线程上下文切换,对资源消耗较大。synchronized就是悲观锁的一种实现。
乐观锁:
如名一样,每次操作都认为不会发生冲突,尝试执行,并检测结果是否正确。如果正确则执行成功,否则说明发生了冲突,回退再重新尝试。乐观锁的过程可以分为两步:冲突检测 和 数据更新。在java多线程中乐观锁一个常见实现即:cas操作。
cas
cas,(compare-and-swap,比较和替换)。其具有三个操作数:内存地址v,旧的预期值a,新的预期值b。当v中的值和a相同时,则用新值b替换v中的值,否则不执行更新。(ps:上述的操作是原子性的,因为过程是:要么执行更新,要么不更新)
在jdk1.5新增的java.util.concurrent(j.u.c) 就是建立在cas操作上的。cas是一种非阻塞的实现(ps:乐观锁采用一种 “自旋锁”的技术,其原理是:如果存在竞争,则没有获得资源的线程不立即挂起,而是采用让线程执行一个忙循环(自旋)的方式,等待一段时间看是否能获得锁,如果超出规定时间再挂起),所以j.u.c在性能上有很大的提升。下面以j.u.c下的atomicinteger的部分源码为例,看一下cas的过程究竟如何。
public class atomicinteger extends number implements java.io.serializable { private volatile int value; public final int get() { return value; } public final boolean compareandset(int expect, int update) { return unsafe.compareandswapint(this, valueoffset, expect, update); } // cas操作 public final int getandincrement() { for (;;) { int current = get(); int next = current + 1; if (compareandset(current, next)) return current; } } }
以上截取自atomicinteger的部分源码。cas操作的核心就在 getandincrement()方法中,在此方法调用的compareandset(int expect , int update) 的两个参数可知,当current值符合expect时,用next替换了current。如果不符合,则在for中不断尝试知道成功为止。在这里的步骤,就相当于一个原子性的 ++i 了。(ps: 单纯的 ++i 不具备原子性)
其中,compareandset方法的实现得益于硬件的发展:多条步骤的操作行为可以通过一条指令完成。(在此是利用jni来完成cpu指令的操作)
cas存在的问题
1. aba问题
aba问题值,内存地址v的值是a,线程one从v中取出了a,此时线程two也从v中取出了a,同时将a修改为b,但是又因为一些原因修改为a。而此时线程one仍看到v中的值为a,认为没有发生变化,此为aba问题。解决aba问题一种方式是通过版本号(version)。每次执行数据修改时,都需要带上版本号,如:1a,2b,3a。通过比较版本号可知是否有发生过操作,也就解决了aba问题。
2. 未知的等待时长
因为cas采取失败重试的策略,所以不确定会发生多少次循环重试。如果在竞争激烈的环境下,其重试次数可能大幅增加。此时效率也就降低了。
总结
乐观锁、悲观锁是一种思想,cas是乐观锁的一种实现。前者是非阻塞同步,非独占,而后者是阻塞同步,独占锁。在可预知的情况下,如果竞争冲突发生较少,乐观锁是个不错的选择。而如果竞争激烈,悲观锁应得到考虑。
关于对象的创建分配内存,因为多线程分配对象空间并不安全,如分配a,b两对象,当给a分配内存时,指针还没修改,就切换到给b分配同一块内存,引发错误。其中一种解决方法就是通过底层的cas操作来保证分配的原子性。
如有错误,敬请斧正,以防误导他人。
参考:http://www.importnew.com/20472.html
上一篇: 设计模式 | 简单工厂模式