CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
一、前言
了解了一下JDK1.8
中ConcurrentHashMap
的实现,发现它实现的主要思想就是依赖于CAS
机制。
JDK并发包里推出了一个ConcurrentHashMap,他默认实现了线程安全性
在JDK 1.7以及之前的版本里,分段
[数组1] , [数组2],[数组3] -> 每个数组都对应一个锁,分段加锁
JDK 1.8以及之后,做了一些优化和改进,锁粒度的细化
[一个大的数组],数组里每个元素进行put操作,都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,采取的是CAS的策略
同一个时间,只有一个线程能成功执行这个CAS,就是说他刚开始先获取一下数组[5]这个位置的值,null,然后执行CAS,线程1,比较一下,put进去我的这条数据,同时间,其他的线程执行CAS,都会失败
分段加锁,通过对数组每个元素执行CAS的策略,如果是很多线程对数组里不同的元素执行put,大家是没有关系的,如果其他人失败了,其他人此时会发现说,数组[5]这位置,已经给刚才又人放进去值了
就需要在这个位置基于链表+红黑树来进行处理,synchronized(数组[5]),加锁,基于链表或者是红黑树在这个位置插进去自己的数据
如果你是对数组里同一个位置的元素进行操作,才会加锁串行化处理;如果是对数组不同位置的元素操作,此时大家可以并发执行的
二、正文
2.1 乐观锁与悲观锁
在讲CAS
之前,先来理解两个概念,即乐观锁和悲观锁:
- 乐观锁:在并发下对数据进行修改时保持乐观的态度,认为在自己修改数据的过程中,其他线程不会对同一个数据进行修改,所以不对数据加锁,但是会在最终更新数据前,判断一下这个数据有没有被修改,若没有被修改,才将它更新为自己修改的值;
- 悲观锁:在并发下对数据进行修改时保持悲观的态度,认为在自己修改数据的过程中,其他线程也会对数据进行修改,所以在操作前会对数据加锁,在操作完成后才将锁释放,而在释放锁之前,其他线程无法操作数据;
CAS
其实就是乐观锁的一种实现方式,而悲观锁比较典型的就是Java
中的synchronized
。下面我就来详细介绍一下CAS
的相关概念。
2.2 什么是CAS?
CAS
全称compare and swap
——比较并替换,它是并发条件下修改数据的一种机制,包含三个操作数:
- 需要修改的数据的内存地址(V);
- 对这个数据的旧预期值(A);
- 需要将它修改为的值(B);
CAS的操作步骤如下:
- 修改前记录数据的内存地址V;
- 读取数据的当前的值,记录为A;
- 修改数据的值变为B;
- 查看地址V下的值是否仍然为A,若为A,则用B替换它;若地址V下的值不为A,表示在自己修改的过程中,其他的线程对数据进行了修改,则不更新变量的值,而是重新从步骤2开始执行,这被称为自旋;
通过以上四个步骤对内存中的数据进行修改,就可以保证数据修改的原子性。CAS
是乐观锁的一种实现,所以这里介绍的步骤和乐观锁的定义差不多,还是很好理解的。
2.3 Java中CAS的使用
Java
中大量使用的CAS
,比如,在java.util.concurrent.atomic
包下有很多的原子类,如AtomicInteger
、AtomicBoolean
......这些类提供对int
、boolean
等类型的原子操作,而底层就是通过CAS
机制实现的。比如AtomicInteger
类有一个实例方法,叫做incrementAndGet
,这个方法就是将AtomicInteger
对象记录的值+1
并返回,与i++
类似。但是这是一个原子操作,不会像i++
一样,存在线程不一致问题,因为i++
不是原子操作。比如如下代码,最终一定能够保证num
的值为200
:
我们看看incrementAndGet
方法的源码:
这里使用了一个unsafe
对象,而unsafe
对象是什么呢?我们知道,Java
并不能像C或C++
一样,直接操作内存,但是JVM
为我们提供了一个后门,就是sun.misc.Unsafe
类,这个类为我们实现了很多硬件级别的原子方法,当然,这些方法都是native
方法,使用其他语言实现,而不是Java
方法。而上面的另外一个变量valueOffset
就是我们需要修改的变量在内存中的偏移量。也许上面这个方法并不能让你感觉使用了CAS
,那再看看下面这个方法:
compareAndSet
是AtomicInteger
的另一个方法,它的作用就是给定一个预期的旧值expect
,以及需要更新为的值update
,若当前变量的值是expect
,就将其修改为update
,否则不修改(这不就是CAS的思想吗)。而它底层调用了unsafe
对象的compareAndSwapInt
方法,从这个名字可以看出,它的实现使用的就是CAS
。compareAndSwapInt
的三个参数valueOffset
、expect
以及update
,刚好对应了CAS
操作的三个操作数。
2.4 CAS机制的ABA问题
CAS
机制虽然简单,但是也存在一些缺陷,其中比较典型的就是ABA
问题。什么是ABA
问题,我简单介绍一下:
- 假设有三个线程
T1
、T2
和T3
,它们都要对一个变量num
的值进行修改,且使用的都是CAS
机制进行同步,假设num
的初始值为100
; - 线程
T1
首先读取了num
的值,将它记录为旧预期A1 = 100
,然后它想要将num
的值修改为80
,记录B2 = 80
,在执行num = B2
前,线程发生了切换,切换到线程T2
; - 假设
T2
毫无阻碍地修改了num
的值,将它从100
修改为80
,然后线程再度切换,T3
开始执行; T3
也是毫无阻碍地修改了num
,将它从80
重新修改为100
,线程再次切换回T1
;T1
从上次运行的断点恢复,也就是准备用B1
的值覆盖num
,但是由于CAS
机制,它需要先检测num
的值是否等于它记录的预期值A1
,然后它发现A1 = num = 100
,认为num
没有被修改过,于是用B1
覆盖了num
;
上面这种情况就是CAS
的ABA
问题:一个变量被修改,但是又被改了回去,在CAS
机制中,将无法察觉这种错误的现象。在线程T1
被中断的过程中,num
的值被修改,按照CAS
的原则,T1
应该放弃对num
的修改,从头开始执行。有人可能想问,修改回去之后,不就和没修改一样吗,有什么影响呢?
??????
对于ABA
问题的解决方案也非常简单,那就是再添加一个变量——版本号。每个变量都加上一个版本号,在它被修改时,也同步修改版本号,而CAS
操作在修改前记录版本号,若在最后更新变量时,记录的版本号与当前版本号一致,表示没有被修改,可直接更新。
2.5 CAS的优缺点以及适用场景
(1)优点
前面也提到过,CAS
是一种乐观锁,其优点就是不需要加锁就能进行原子操作;
(2)缺点
CAS
的缺点主有两点:
CAS
机制只能用在对某一个变量进行原子操作,无法用来保证多个变量或语句的原子性(synchronized
可以);- 假设在修改数据的过程中经常与其他线程修改冲突,将导致需要多次的重新尝试;
(3)适用场景
由上面分析的优缺点可以看出,CAS
适用于并发冲突发生频率较低的场合,而对于并发冲突较频繁的场合,CAS
由于不断重试,反倒会降低效率。
三、总结
CAS
是一种在并发下实现原子操作的机制,但是只能用来保证一个变量的原子性,适用于并发冲突频率较低的场合。
四、参考
推荐描述CAS的博客
- https://mp.weixin.qq.com/s/f9PYMnpAgS1gAQYPDuCq-w
- https://mp.weixin.qq.com/s/nRnQKhiSUrDKu3mz3vItWg
- https://www.cnblogs.com/tuyang1129/p/12585019.html