CAS(比较和设置)算法是硬件对于并发操作的实现.(CPU指令级的操作,只有一步原子操作) 无锁的非阻塞算法 CAS算法保证同时访问时只有一个线程能进来,当多个线程同时并发访问操作共享数据的时候,有且只有一个能够成功,其他的线程都会失败,会尝试当前操作直到没有冲突为止..CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B.当且仅当预期值阿和内存值V相同时,将内存值V修改为B,否则什么都不做。类似于乐观锁。CAS自旋的概率会比较大,从而浪费更多的CPU资源。
CAS的缺点:
1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性(多个变量)
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
3.ABA问题
这是CAS机制最大的问题所在。
什么是ABA问题?
引用原书的话:如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令就可能出现这种问题,在CAS操作中将判断“V的值是否仍然为A?”,并且如果是的话就继续执行更新操作,在某些算法中,如果V的值首先由A变为B,再由B变为A,那么CAS将会操作成功。但是多数的场景(比如高效的计数器实现)是不用担心这个问题的
怎么避免ABA问题?
Java中提供了AtomicStampedReference和AtomicMarkableReference来解决ABA问题。
JDK如何实现CAS操作
我们接下来看看JDK自带的原子类的AtomicInteger的源码,下面贴出关键代码
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
首先可以看到AtomicInteger类在域中声明了这两个私有变量unsafe和valueOffset。其中unsafe实例采用Unsafe类中静态方法getUnsafe()得到,但是这个方法如果我们写的时候调用会报错,因为这个方法在调用时会判断类加载器,我们的代码是没有“受信任”的,而在jdk源码中调用是没有任何问题的;valueOffset这个是指类中相应字段在该类的偏移量,在这里具体即是指value这个字段在AtomicInteger类的内存中相对于该类首地址的偏移量。
然后可以看一个有一个静态初始化块,这个块的作用即是求出value这个字段的偏移量。具体的方法使用的反射的机制得到value的Field对象,再根据objectFieldOffset这个方法求出value这个变量内存中在该对象中的偏移量。
public final int incrementAndGet() {
for (;;) {
int current = get(); //获取当前变量最新值
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
//第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的valueOffset的值),
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
incrementAndGet()方法实现了自增的操作。核心实现是先获取当前值和目标值(也就是值+ 1),如果compareAndSet(current,next)返回成功则该方法返回目标值。
那么compareAndSet是做什么的呢?
理解这个方法我们需要引入CAS操作AtomicInteger中的CAS操作就是compareAndSet(),其作用是每次从内存中根据内存偏移量(valueOffset)取出数据,将取出的值跟望比较,如果数据一致就把内存中的值改为更新。
这样使用CAS就保证了原子操作。其余几个方法的原理跟这个相同。