什么是CAS?
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
乐观锁(多读场景):乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:
悲观锁(多写场景):当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。-----悲观并发控制实际上是“先取锁再访问”的保守策略
package lin.cas;
import java.util.concurrent.atomic.AtomicInteger;
//CAS compareAndSet:比较并交换
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020); //2020是一个初始值
//源码: public final boolean compareAndSet(int expect, int update)
// 期望 更新
atomicInteger.compareAndSet(2020,2021); //如果我期望的值达到了,就更新,否则就不更新
System.out.println(atomicInteger.get());
}
}
结果:
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么执行操作!如果不是,就一直循环!(因为底层是自旋锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
CAS缺点:
- 循环会耗时。CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断(因为底层是自旋锁)的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。
- 由于底层是一个CPU的操作,一次性只能保证一个共享变量的原子性
- 会存在ABA问题(狸猫换太子):两条线程同时去操作一个资源Q=1,线程A先执行,操作了资源Q,使用cas(1,2)改成了 Q=2,然后在cas(2,1)改成了Q=1,这时候线程B拿到的资源Q=1,就是已经被动过的资源
package lin.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ABADemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020); //2020是一个初始值
//线程A
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" loaded");
atomicInteger.compareAndSet(2020,2021);
atomicInteger.compareAndSet(2021,2020);
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程B
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" loaded");
atomicInteger.compareAndSet(2020,6666);
},"B").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicInteger.get());
}
}
ABA问题(有两类)
第一类问题
我们考虑下面一种ABA的情况:
- 在多线程的环境中,线程a从共享的地址X中读取到了对象A。
- 在线程a准备对地址X进行更新之前,线程b将地址X中的值修改为了B。
- 接着线程b将地址X中的值又修改回了A。
- 最新线程a对地址X执行CAS,发现X中存储的还是对象A,对象匹配,CAS成功。
上面的例子中CAS成功了,但是实际上这个CAS并不是原子操作,如果我们想要依赖CAS来实现原子操作的话可能就会出现隐藏的bug。
第一类问题的关键就在2和3两步。这两步我们可以看到线程b直接替换了内存地址X中的内容。
在拥有自动GC环境的编程语言,比如说java中,2,3的情况是不可能出现的,因为在java中,只要两个对象的地址一致,就表示这两个对象是相等的。
2,3两步可能出现的情况就在像C++这种,不存在自动GC环境的编程语言中。因为可以自己控制对象的生命周期,如果我们从一个list中删除掉了一个对象,然后又重新分配了一个对象,并将其add back到list中去,那么根据 MRU memory allocation算法,这个新的对象很有可能和之前删除对象的内存地址是一样的。这样就会导致ABA的问题。
第二类问题
如果我们在拥有自动GC的编程语言中,那么是否仍然存在CAS问题呢?
考虑下面的情况,有一个链表里面的数据是A->B->C,我们希望执行一个CAS操作,将A替换成D,生成链表D->B->C。考虑下面的步骤:
- 线程a读取链表头部节点A。
- 线程b将链表中的B节点删掉,链表变成了A->C
- 线程a执行CAS操作,将A替换从D。
最后我们的到的链表是D->C,而不是D->B->C。
问题出在哪呢?CAS比较的节点A和最新的头部节点是不是同一个节点,它并没有关心节点A在步骤1和3之间是否内容发生变化。
AtomicStampedReference(原子引用)------有时间戳的原子引用
作用:可以解决第一类ABA问题,当线程A修改过了值以后,时间戳就会发生变化,后面其他线程再去使用这个值的时候,就知道是被线程A修改过后的值了。--------------------跟乐观锁的原理是一样的
举例:使用有时间戳的原子引用,来实现CAS
package lin.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
public static void main(String[] args) {
// AtomicInteger atomicInteger = new AtomicInteger(2020); //2020是一个初始值
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);//这个“1”就是时间戳(版本号)
//线程A
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" loaded");
// atomicStampedReference.getStamp();//获得版本号
System.out.println(Thread.currentThread().getName()+" StampA1->"+atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" StampA2->"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" StampA3->"+atomicStampedReference.getStamp());
},"A").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程B
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" loaded");
System.out.println(Thread.currentThread().getName()+" StampB1->"+atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,6666,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" StampB2->"+atomicStampedReference.getStamp());
},"B").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.getReference());
}
}
这里有一个大坑 AtomicStampedReference<Integer>,特别是Integer是,默认的大小范围在-127~+127之间,不然CAS时,不会成功!
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(2020, 1);//这个“1”就是时间戳(版本号)
此时2020就已经超过了Integer的定义范围,之后的CAS操作返回值都为False!!!
注意:如果这其中的泛型是一个包装类时,就要注意对象的引用问题(正常的业务操作,泛型中都是一个个对象,比较的就是一个个对象)
AtomicStampedReference<User> atomicStampedReference = new AtomicStampedReference<>(User, 1);//正常的业务操作时,泛型中基本都是一个个对象,比较的也是一个个对象