CAS全称叫做 Compare and swap (比较和交换),CAS无锁机制是乐观锁的一种,也叫自旋锁,CAS假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则重新再来一次,无限循环地进行对比,直到没有冲突为止;
Atomic 类就是使用CAS无锁机制实现的类;
CAS操作依赖底层硬件的CAS指令,CAS指令有两个步骤:冲突检测和更新操作,但是这两个步骤合起来成为一个原子性操作。
CAS指令需要3个操作数:
- 需要更新的变量(V)(主内存)
- 旧的预期值(E)(本地内存)
- 新值(B)
CAS指令执行时,首先比较内存位置V(主内存)处的值和E(本地内存)的值是否相等(冲突检测),如果相等,就用新值B(新值)覆盖V(更新操作),否则,就什么也不做。所以,一般循环执行CAS操作,直到成功为止。执行流程如下图
JDK 1.5 之后引进了一个叫做原子类的东西,它们都是以 Atomic 开头的,就拿 AtomicInteger 类来说,底层执行原理如下图:
原子类的常用方法如下
- // 以原子方式将给定值与当前值相加,可用于线程中的计数使用,(返回更新的值)。
- int addAndGet(int delta)
-
- // 以原子方式将给定值与当前值相加,可用于线程中的计数使用,(返回以前的值)
- int getAndAdd(int delta)
-
- // 以原子方式将当前值加 1(返回更新的值)
- int incrementAndGet()
-
- // 以原子方式将当前值加 1(返回以前的值)
- int getAndIncrement()
-
- // 以原子方式设置为给定值(返回旧值)
- int getAndSet(int newValue)
-
- // 以原子方式将当前值减 1(返回更新的值)
- int decrementAndGet() :
-
- // 以原子方式将当前值减 1(返回以前的值)
- int getAndDecrement()
-
- // 获取当前值
- get()
还是写个代码例子出来吧
- package com.test;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.atomic.AtomicInteger;
- /*
- 原子类测试
- */
- public class AtomicThreadTest {
-
- //普通int类型
- static int intNum = 0;
- // 原子类
- static AtomicInteger num = new AtomicInteger(0);
-
- public static void main(String[] args) {
-
- // 线程数量
- int threadNum = 10;
-
- ExecutorService executorService = Executors.newCachedThreadPool();
- // 创建多个线程进行++运算
- for (int j = 0; j < threadNum; j++) {
- executorService.execute(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < 1000; i++) {
- // 每次将数字 + 1,相当于 i++
- num.incrementAndGet();
- intNum++;
- }
- }
- });
- }
-
- // 关闭线程池指令,不在接受新任务,待原有任务执行完成后关闭线程池
- executorService.shutdown();
-
- System.out.println("原子类结果:" + num);
- System.out.println("普通int类型结果:" + intNum);
- }
- }
看上面的图和例子还是有些抽象,那我们在看看下面这张图,3个线程的执行顺序和内部活动具体执行情况都清楚地描述出来了;
CAS 存在的问题
cas 虽然可以很高效地解决原子操作,但是仍然会有三个问题
- ABA问题
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
1、ABA问题
因为cas会在更新的时候检查值是否发生变化,没有变化才会更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS在检查的时候就会认为它的值没有发生变化,但实际上是有变化的。
ABA问题如何解决?
ABA的问题的解决思路是使用版本号 version,每次变量更新的时候把版本号 加一,那么 A-B-A 就会变成 A1-B2-A3;可以使用AtomicStampedReference 来解决ABA问题,AtomicStampedReference 可以存放对象,在对象内增加一个version的属性即可;数据库的乐观锁也是用version版本号解决的,在表内增加一个version的字段,每次更新数据时 version + 1, 更新前先比较原版本号是否一致;sql语句如下:
update table set data = '更新的数据' , version = version + 1 where version = 1
2、循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
cas本身具备原子性吗?
答:cas本身是具备原子性,cas本身锁的操作是在hotSpot源码中实现的,也就是c++层面的,在hotSpot中的cas逻辑中有这么一行代码:
#define LOCK_IF_MP(mp)
这句代码是什么意思呢? 意思是如果你有好多个cpu,就执行如下代码:
lock cmpxchg
这个代码意思是lock compare exchange ,锁住这个cpu的北桥信号,属于硬件级别的锁;
但如果只有一个CPU,就只执行以下代码:
cmpxchg
cas底层上锁流程图如下