前言
CAS机制听起来很高大上,其实就可以把他理解为与synchronized并列的一种方式。我个人把他理解为是实现线程同步的另外一种方式(虽然本质上是异步访问,但是最终的结果与同步访问的结果是一样的)或者说,从微观上来看是异步的,但是从宏观上来看是同步的。就类似并发一样,微观上是每一时刻只执行一个进程,但是宏观上来看是多进程并发,一个道理。
1.为啥要用CAS机制?
在正式介绍CAS之前,不如聊聊它与synchronized的区别,或者它的特点,为啥要用CAS机制?
那就不得不提到悲观锁和乐观锁的概念了。
所谓悲观锁,就类似于synchronized这样的,多个线程访问被synchronized修饰的代码块,谁抢到锁,谁就执行。所以synchronized默认是高并发并且竞争很激烈,所以它很“悲观”。而乐观锁就恰恰相反,它认为程序的竞争不激烈,没有人会跟我竞争这个变量,所以它就很“乐观”。而CAS操作的锁就是乐观锁的典型例子。
悲观锁如synchronized的效率其实是相对不高的,因为上下文切换需要耗费很多资源,而乐观锁的效率在大部分情况下还是很高的。我们看这张图
lock就是说的synchronized,AtomicInteger就是乐观锁。可以看到当线程数目多于2时,CAS机制的性能一般来讲就优于synchronized了。
2.CAS原理
CAS的全称是Compare And Swap,就是比较 和 交换。这是CAS的核心。
CAS机制当中使用了3个基本操作数:内存地址(我们不用管),旧的预期值A,要修改的新值B。在正式修改变量之前,它要将预期值A于在相应内存地址的实际值进行比较,如果相等,则将新值B替换到内存地址的实际值,如果不相等,则将此时的实际值作为新的预期值,然后再循环。
什么意思,多说无益,我们举个实际点的例子吧。比如说有一个变量number,初始值为0,然后有两个线程对其进行number++操作。让我们来看一下此时CAS的工作原理
假如线程1 的操作流程是这样的(这里在预期的操作那里搞错了,是得到新值1,预期的旧值为0)
那么线程2 的操作流程就是这样的
此时number完成了两次++,变为了2
整体流程也可以参考这张图
3.CAS使用
CAS的核心就是compare 和 swap 的那一段原子操作,而JDK中相关原子操作类基本都是以Atomic开头的。
使用示例
private static AtomicInteger number = new AtomicInteger(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 (Exception e) {
e.printStackTrace();
}
//每个线程让number自增100次
for (int i = 0; i < 100; i++) {
//以下incrementAndGet方法就保证了number能够在CAS机制下进行++
number.incrementAndGet();
//这个方法和getAndIncrement的区别类似于C语言中++i 和 i++的区别
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(number);
}
这里就能够百分之百确定能够打印出200,所以能确定其进行了原子操作。
常用的原子操作类
4.CAS存在的问题
什么东西有利必有弊,CAS机制也是如此。其缺点主要为三方面
①ABA问题
这个问题是针对于compare的。在之前的原理讲解中我们说过,compare的时候,如果预期的旧值与当前实际值相同,则进行swap。可是这个相同,是值结果相同,而不是这个值自始至终没有变过。就像上面那个number的例子。假如有线程3以极快的速度,将number从0改为了1又改为了0,那么线程1到compare的时候发现compare还是0,就认定compare没有被改过,但实际上是改过的。这就是ABA问题。比如我喝了别人的水,然后又给他盛满放回原处,我不跟他说,他可能就认为他的杯子没有被人动过,其实是被人动过的。
解决:这个问题可以解决,就是设置一个版本戳,来监听对象的变化
箭头的这两个方法就是具体实现类。其中,AtomicMarkableReference与AtomicStampedReference的区别是:前者是监听这个对象变没变,后者是监听这个对象被动了几次。
②开销问题
这是CAS一定会有的自己的缺陷,因为这个自旋,就把他想成死循环。死循环也是挺耗费资源的。如果长期不成功,CPU的开销还是很大的。
③只能保证一个共享变量的原子操作
这个也可以解决,就是通过这个类AtomicReference。把需要保证原子操作的多个共享变量封装成一个类,然后创建对象作为AtomicReference的参数就可以,改的时候直接换对象就行。
比如这样