简述
CAS(Compare and Swap,比较并交换)是一个CPU指令,其语义为:“如果地址V上的值和期望的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
同synchronized相比
Synchronized是一个独占锁,属于悲观锁(假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。即在修改数据之前先锁定,再修改的方式),而CAS更类似于乐观锁(假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。)
锁与CAS的缺点
传统锁的缺点
- 锁引起线程的阻塞,对于没有能占用到锁的线程或者进程将会一直等待锁的占有者释放资源后才能继续。
- 申请和释放锁的操作增加了很多访问共享资源的消耗。
- 锁不能很好的避免编程开发者设计实现的程序出现死锁或者活锁可能。
- 优先级低的线程可能先获取锁,导致优先级高的线程等待,出现优先级反转的问题。
CAS存在的缺点
- CPU开销过大:由于是乐观锁,因此一般会用自旋来检测CAS操作是否成功,当操作没有成功时,线程会反复尝试更新一个变量,这样会导致一直消耗CPU。
- ABA问题:假设要更新一个A值为B,这时候有另一个线程,已经把A改为了C,然后又改成了A,那么操作还是会成功,也就是说CAS没办法发现其实已经有其他线程进行了更新。(就像一个人倒了一杯水,然后走开了,这时候这杯水被另一个人喝完了,然后他又重新到了一杯,放回原处,此时原本这杯水的主人并不会发现这杯水已经被换过了)
使用场景
- 使用CAS:当对于共享资源的竞争比较少时,这时候如果使用锁,会过多消耗cpu资源(上下文切换),而CAS基于操作借助C来调用CPU底层指令实现的,不需要进入内核,不需要切换线程,并且由于竞争少,所以自旋几率也低,因此可以有更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
JAVA中的CAS类
更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
使用(AtomicInteger为例子)
我们都知道,在使用多线程对共享资源进行累加时会出现错误的情况。例如:
public class CAS {
private static class Thread1 implements Runnable {
int count;
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 run = new Thread1();
Thread thread1 = new Thread(run);
Thread thread2 = new Thread(run);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("执行完毕count为:" + run.count);
}
}
结果:
这是由于count++
并不是一个原子操作。
synchronized
可以使用synchronized进行改进。
public class CAS {
private static class Thread1 implements Runnable {
int count;
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 5000; i++) {
count++;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 run = new Thread1();
Thread thread1 = new Thread(run);
Thread thread2 = new Thread(run);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("执行完毕count为:" + run.count);
}
}
结果:
AtomicInteger
public class CAS {
private static class Thread1 implements Runnable {
AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 5000; i++) {
atomicInteger.getAndIncrement();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 run = new Thread1();
Thread thread1 = new Thread(run);
Thread thread2 = new Thread(run);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("执行完毕count为:" + run.atomicInteger.get());
}
}
结果:
解决ABA问题
AtomicMarkableReference,AtomicStampedReference这两个类是用来解决ABA问题的,原理就是通过版本号来识别是否发生过修改。
AtomicMarkableReference与AtomicStampedReference区别
AtomicMarkableReference的版本号为布尔值,只关注有没有发生修改
AtomicStampedReference的版本号为数值,可以关注发生了几次修改