CAS

CAS概述

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统(一般都是多核系统),如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

CAS乐观锁

CAS 全称为 Compare And Swap 翻译过来就是比较并且交换

  • Synchornized 是悲观锁,线程一旦得到锁,其他的线程就只能挂起了
  • CAS 的操作则是乐观锁,他认为自己一定会拿到锁,所以他会一直尝试,直到成功拿到为止;

CAS 机制

在看到 Compare 和 Swap 后,我们就应该知道,CAS 里面至少包含了两个动作,分别是比较和交换,在现在的 CPU 中,为这两个动作专门提供了一个指令,就是CAH 指令,由 CPU 来保证这两个操作一定是原子的,也就是说比较和交换这两个操作只能是要么全部完成,要么全部没有完成

Java中CAS实现

来看一个方法compareAndSet

点开方发现是一个Unsafe类的compareAndSwapInt方法

参数:(当前要操作的对象,要操作对象中属性地址的偏移量,需要修改数据的期望值,需要修改的新值)

Java中一般是这三个方法

Unsafe

Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地〈native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像c的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

再次强调!由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

CAS核心思想:比较要更新变量的值V和要预期值E,相等才会将V的值设为新值N,如果不相等自选再来

AtomicReference

我们看了AtomicInteger、AtomicLong、AtomicBoolean等等方法,那能不能所以类型都有原子类呢?当然可以——AtomicReference!

1
Class AtomicReference<V>

看到V就知道可以装任意类型进去了

定义一个User类

1
2
3
4
5
6
7
8
@Getter
@Setter
@ToString
@Builder
class User{
private String name;
private Integer age;
}
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
AtomicReference<User> atomicReference = new AtomicReference<>();
User pjf = new User("彭建飞",21);
User xxx = new User("xxx",21);
atomicReference.set(pjf);

atomicReference.compareAndSet(pjf,xxx);
System.out.println(atomicReference.get());
}

输出:

1
User(name=xxx, age=21)

CAS与自旋锁

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性以达到锁的效果,至于自选看字面意思就能明白,自己旋转(while),是指尝试获取锁的线程不会立即阻塞,而是采取循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

1
2
AtomicInteger atomicInteger = new AtomicInteger(1);
atomicInteger.incrementAndGet();

循环自选

自定义自旋锁

题目:实现一个自旋锁,复习CAS思想

自旋锁的好处:循环比较获取没有类似wait的阻塞

通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,所以只能通过自选等待,直到A释放锁后B随后抢到

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
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();

public void lock() throws InterruptedException {
Thread currentThread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+" come in");
while(!atomicReference.compareAndSet(null,currentThread)){
Thread.sleep(10);
}
}

public void unLock(){
atomicReference.compareAndSet(Thread.currentThread(),null);
System.out.println(Thread.currentThread().getName()+"task over");
}

public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();

new Thread(()->{
try {
spinLockDemo.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unLock();
},"A").start();

new Thread(()->{
try {
spinLockDemo.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}

spinLockDemo.unLock();
},"B").start();
}
}

CAS缺点

循环时间长CPU开销大

一直靠循环去竞争获取锁,CPU开销大

ABA问题

CAS会导致“ABA”问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。

尽管线程1的CAS操作成功,但不是代表这个过程就是没有问题的

分析为什么普通CAS会有ABA问题,因为我们比较的时候只比较了内容,那么这里可以解决的办法就是加上时间戳记(Stamp),在我们的原子类中有一个AtomicStampedReference类,在AtomicReference之上加了一个Stamped

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
public class AtomicStampReference {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+" "+"首次版本号:"+stamp);
//暂停500毫秒保证t4线程初始化拿到的版本号一样
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}

atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" "+"第二次次版本号:"+atomicStampedReference.getStamp());

atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" "+"第二次次版本号:"+atomicStampedReference.getStamp());
},"t3").start();

new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+" "+"首次版本号:"+stamp);
//暂停1秒保证t4线程初始化拿到的版本号一样
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

boolean b = atomicStampedReference.compareAndSet(100, 2023, stamp, stamp+ 1);
System.out.println(b+" "+atomicStampedReference.getReference()+" "+"第二次次版本号:"+atomicStampedReference.getStamp());

},"t4").start();
}
}

CAS
http://example.com/2023/06/12/CAS/
Author
Posted on
June 12, 2023
Licensed under