一、为什么需要CAS?
为什么需要CAS呢?我们知道java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。比如我们使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,保证了其可见性和有序性但却无法保证其原子性。我们先看下面一个例子:
public class ThreadCas {
public static volatile int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(
new Runnable() {
public void run() {
try {
for (int j = 0; j < 10; j++) {//每个线程对count自增20
System.out.println(count++);
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
上面例子我们使用5个线程对count进行增加,为了保证可见性和有序性还使用了volatile关键字对count进行修饰。正常我们预期的结果是50,但实际却不是,可以看下运行后的结果:
结果很明显,和想象的不一样,出现的两个46相同值,最终结果不是50。为什么会出现这个问题呢?这是因为变量count虽然保证了可见性和有序性,但是缺没有保证原子性。其原因我们可以来分析一下。
对于count++操作其实可以细分成三个步骤。
(1)从内存中读取count
(2)对count进行加1操作
(3)将count的值重新写入内存中
在单线程状态下上面的操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对count进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值,造成了线程的不安全现象。如何去解决这个问题呢?最常见的方式就是使用AtomicInteger来修饰count,AtomicInteger的作用就是为了保证原子性。下面我们用AtomicInteger重新测试下:
public class ThreadCas {
public static AtomicInteger count = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(
new Runnable() {
public void run() {
try {
for (int j = 0; j < 10; j++) {//每个线程对count自增20
System.out.println(count.incrementAndGet());
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
执行后结果如下:
上面的结果无论执行多少次,最终的值都是50。那么incrementAndGet是如何实现的呢?我们下面就来简单分析底层源码。
二、CAS底层原理
换句话说也就是CAS为什么能保证原子性?
(1)靠的是底层的Unsafe类
(2)Unsafe类是CAS的核心类,由于java无法直接访问底层系统,需要通过本地(native)方法访问,Unsafe相当于一个后门,该类可以直接操作特定的内存数据。 Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中的CAS依赖于Unsafe类中的方法
(3)注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,Unsafe类中的native方法是调用底层原语,原语是有原子性的。
下面让我们进入源码分析吧:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
看到这里,我们可以发现底层使用的是sun.misc包下unsafe的getAndAddInt方法,但是现在我们还看不出什么,我们再深入到源码中看看getAndAddInt方法又是如何实现的
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
到了这一步就稍微有点眉目了,原来底层调用的是compareAndSwapInt方法,这个compareAndSwapInt方法其实就是CAS机制。因此如果我们想搞清楚AtomicInteger的原子操作是如何实现的,我们就必须要把CAS机制搞清楚,这也是为什么我们需要掌握CAS机制的原因。我们继续往下看compareAndSwapInt方法:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
我们可以看到这里面主要有四个参数,第一个参数就是我们操作的对象count,第二个参数是对象count的地址偏移量,第三个参数表示我们期待这个count是什么值,第四个参数表示的是count的实际值。
不过这里我们会发现这个compareAndSwapInt是一个native方法,也就是说再往下走就是C语言代码。这里就不继续深入研究了,有兴趣的同学可以通过openjdk继续往下研究底层C源码,会发现CAS真正实现机制由操作系统的汇编指令完成的。
三、CAS含义
CAS全称Compare-And-Swap(比较并交换),主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
1、变量内存地址,V表示
2、旧的预期值,A表示
3、准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其他线程均会失败。失败线程会重新尝试或将线程挂起(阻塞)
我们使用一个例子来解释相信你会更加的清楚:
比如说给你儿子订婚。你儿子就是内存位置,你原本以为你儿子是和杨贵妃在一起了,结果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你预想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。现在你应该明白了吧。
四、CAS优点
cas是一种乐观锁,而且是一种非阻塞的轻量级的乐观锁,什么是非阻塞式的呢?其实就是一个线程想要获得锁,对方会给一个回应表示这个锁能不能获得。在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。
五、CAS缺点
缺点 | 说明 | 解决方法 |
---|---|---|
ABA问题 | ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。具体什么场景下要解决CAS导致的ABA问题?如何解决?我会在下个章节中讲到。 举个例子解释说明下ABA: 你有一瓶水放在桌子上,别人把这瓶水喝完了,然后重新倒上去。你再去喝的时候发现水还是跟之前一样,就误以为是刚刚那杯水。如果你知道了真相,那是别人用过了你还会再用嘛? | 每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。 Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳(stamp),从而避免ABA问题。 |
循环时间长开销大 | 自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销 | 在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。 |
只能保证一个共享变量的原子操作 | CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。 | 比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。还可以考虑使用AtomicReference来包装多个变量,通过这种方式来处理多个共享变量的情况。 |