CAS机制
先来看一段代码,看一下执行结果是多少?
示例:启动两个线程,每个线程中让静态变量count循环累加100次
public class CAS {
public static int count=0;
public static void main(String[] args) {
//开启两个线程
for(int i=0;i<2;i++){
new Thread(
new Runnable() {
@Override
public void run() {
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
//每个线程当中让count值自增100次
for(int j=0;j<100;j++){
count++;
}
}
}
).start();
}
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("count="+count);
}
}
结果一定是200吗?显示是不一定的,因为是非线程安全的。
解决方案那??
通常情况下我们首先想到的是加synchronized同步锁,如下:
public class CAS {
public static int count=0;
public static void main(String[] args) {
//开启两个线程
for(int i=0;i<2;i++){
new Thread(
new Runnable() {
@Override
public void run() {
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
//每个线程当中让count值自增100次
for(int j=0;j<100;j++){
synchronized (CAS.class) {
count++;
}
}
}
}
).start();
}
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("count="+count);
}
}
结果:显然是没问题的
还有另外一种解决方案,都知道Java中有原子操作类,在java.util.concurrent.atomic包下,一系列以Atomic开头的包装列,例如AtomicBoolean、AtomicInteger、AtomicLong,分别用于Boolean、Integer、Long类型的原子性操作。
修改代码如下:
public class casAtomicInteger {
public static AtomicInteger count=new AtomicInteger();
public static void main(String[] args) {
//开启两个线程
for(int i=0;i<2;i++){
new Thread(
new Runnable() {
@Override
public void run() {
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
//每个线程当中让count值自增100次
for(int j=0;j<100;j++){
count.incrementAndGet();
}
}
}
).start();
}
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("count="+count);
}
}
那么这两种方式有什么区别那?那种性能更好一些那?
对于这种情况来说,原子操作类性能比较好一些,为什么那?下面进行分析。
synchronized关键字会让没有得到锁资源的线程进入Blocked状态,而后在争夺到锁资源后恢复为Runnable状态,过程涉及到操作系统用户模式和内核模式的转换,代价比较高。
Atomic操作类的底层采用了CAS(Compare And Swap)机制。
详细介绍CAS:
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
要更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
例子:
从思想上讲,Synchronized属于悲观锁,悲观地认为程序中并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序的并发情况不那么严重,所以让线程不断去尝试更新。
适用场景
在高并发的情况下,同步锁更适合一些;
Atomic系列类以及Lock系列类的底层实现都采用了CAS机制。
CAS缺点
- CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。 - 不能保证代码块的原子性
CAS机制保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不用Synchronized了。 - ABA问题
这是CAS机制最大的问题所在。
ABA问题,就是一个变量的值从A改成了B,又改成了A.
遵循CAS提款机的例子:
小灰账户余额有100元,要取50,由于提款机硬件出了点小问题,小灰取款的操作同时提交了两次,开启了两个线程,两个线程都要获取当前值100,更新为50。
正常情况下应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。
线程1首先执行成功,把100改成50,线程2因为某种原因阻塞了,这时候,小灰妈妈刚好给小灰汇款50。
线程2仍然阻塞状态,线程3执行成功,把余额从50改成100。
线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把100更新成了50。
换成任何一个人都不干啊,无缘无故少了50块钱。
那么ABA的问题怎么解决那???
加版本号
要做到严谨的CAS机制,我们在Compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
那么我们再看看看CAS的底层是怎么实现的?
以AtomicInteger为例,上源码:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
private volatile int value;
public final int get() {
return value;
}
这段代码是一个无限循环,也就是CAS的自旋。循环体做了三件事:
- 获取当前值
- 当前值+1,计算出目标值
-
进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤。
get方法,获取的变量的当前值。如何保证当前值是内存中的最新值那??上面我们看到了volatile关键字。
再来看一下compareAndSet方法:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
unsafe是JVM提供的一个硬件级别的原子操作
unsafe的compareAndSwapInt方法参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
总结
有不对的地方,欢迎留言!
上一篇: Linux Mutex机制分析
下一篇: CAS机制