0%

Java 并发 - CAS 分析

1.CAS 介绍

什么是CAS?

CAS,全称:Compare and Swap,即比较-替换;CAS是一种无锁算法通过无锁的方式实现了多个线程间变量的同步;CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。

注意:JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。

具体内容:

假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

意思代码如下:

1
2
3
4
5
6
if (this.value == A){
this.value = B;
return true;
} else {
return false;
}

这是一种乐观策略,认为并发操作并不总会发生。

还是不明白?那我再说明下,就比如我现在要修改数据库的一条数据,修改之前我先拿到他原来的值,然后在SQL里面还会加个判断,原来的值和我手上拿到的他的原来的值是否一样,一样我们就可以去修改了,不一样就证明被别的线程修改了你就return错误就好了。

其在SQL 中的伪代码如下:

1
update a set value = newValue where value = #{oldValue} // oldValue 就是我们执行前查询出来的值

底层实现:

留空,后面补上!


2.CAS 存在的问题

CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。

  1. 循环时间太长:如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。
  2. 只能保证一个共享变量原子操作:看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。例如读写锁中state的高地位
  3. ABA问题:就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。

主要问题:怎么解决ABA问题?

  1. 用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样。
  2. Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

下面详细讲讲AtomicStampedReference 中的compareAndSet方法:

AtomicStampedReference的CAS方法

compareAndSet有四个参数,分别表示:预期引用、更新后的引用、预期标志、更新后的标志。源码部门很好理解:预期的引用 == 当前引用,预期的标识 == 当前标识,如果更新后的引用和标志和当前的引用和标志相等则直接返回true,否则通过Pair生成一个新的pair对象与当前pair CAS替换。Pair为AtomicStampedReference的内部类,主要用于记录引用和版本戳信息(标识),定义如下:

pair方法

Pair记录着对象的引用和版本戳,版本戳为int型,保持自增。同时Pair是一个不可变对象,其所有属性全部定义为final,对外提供一个of方法,该方法返回一个新建的Pari对象。pair对象定义为volatile,保证多线程环境下的可见性。在AtomicStampedReference中,大多方法都是通过调用Pair的of方法来产生一个新的Pair对象,然后赋值给变量pair。如set方法:

set方法

使用atomicStampedReference进行解决ABA示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class ABAdemo {
// 真实值
static AtomicReference<Integer> atomicReference = new AtomicReference <>(100);
// 真实值 版本号
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference <>(100,1);
public static void main(String[] args) {

// ABA 问题
new Thread(() -> {
// 修改为101然后又马上修改回来
atomicReference.compareAndSet(100, 101); // B
atomicReference.compareAndSet(101, 100); // A
}, "t1").start();

new Thread(() -> {
// 睡1秒,让t1线程完成了ABA
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();

try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("=====================以下是 ABA 的解决============================================");

new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+atomicStampedReference.getStamp());

// 睡1秒,让t4线程也拿到atomicStampedReference
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// atomicStampedReference 的compareAndSet四个参数: 期望值,想要修改的值,现在版本号,修改之后的版本号
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第二次版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第三次版本号:"+atomicStampedReference.getStamp());
}, "t3").start();


new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+" 修改成功否:"+b);
System.out.println(Thread.currentThread().getName()+"以为的版本号 :"+stamp+" 当前最新的版本号 :"+atomicStampedReference.getStamp());
System.out.println("当前实际最新的值 :" +atomicStampedReference.getReference());
}, "t4").start();
}
}

输出如下

1
2
3
4
5
6
7
8
9
true	2019
=====================以下是 ABA 的解决============================================
t3 第一次版本号:1
t4 第一次版本号:1
t3 第二次版本号:2
t3 第三次版本号:3
t4 修改成功否:false
t4以为的版本号 :1 当前最新的版本号 :3
当前实际最新的值 :100