compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。
它包含三个操作数一一内存位置的值、预期原值及更新值。
执行CAS操作的时候,将内存位置的值与预期原值比较。
如果相匹配,,那么处理器会自动将该位置值更新为新值。
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令 (cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX) 底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁, 这里的排他时间要短很多, 所以在多线程情况下性能会比较好。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
原子引用
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<User> atomicReference=new AtomicReference<>();
User z3=new User("z3",22);
User l4=new User("l4",28);
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3,l4)+"\t"+atomicReference.get().toString());//true User(userName=l4, age=28)
System.out.println(atomicReference.compareAndSet(z3,l4)+"\t"+atomicReference.get().toString());//false User(userName=l4, age=28)
}
}
@Getter
@ToString
@AllArgsConstructor
class User{
String userName;
int age;
}
自旋锁
CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
/*
通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
当前有线程持有锁,所以只能通过自旋等待,知道A 释放锁B抢到
*/
public class SpinLockDemo {
AtomicReference<Thread> atomicReference=new AtomicReference<>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t"+"-----come in");
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void unlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t"+"-----task over");
}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
spinLockDemo.unlock();
},"a").start();
//线程a先与b启动
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
spinLockDemo.lock();
spinLockDemo.unlock();
},"b").start();
}
}
CAS的缺点
循环时间长开销很大。
ABA问题:
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B。然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
ABA问题解决
单线程
public class AtomicStampedDemo {
public static void main(String[] args) {
Book book=new Book(1,"javaBook");
AtomicStampedReference<Book> stampedReference=new AtomicStampedReference<>(book,1);
System.out.println( stampedReference.getReference()+"\t"+stampedReference.getStamp());
//Book(id=1, bookName=javaBook) 1
Book mysql=new Book(1,"mysqlBook");
boolean flag;
flag = stampedReference.compareAndSet(book, mysql, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println( flag+" "+stampedReference.getReference()+"\t"+stampedReference.getStamp());
//true Book(id=1, bookName=mysqlBook) 2
flag = stampedReference.compareAndSet(mysql, book, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println( flag+" "+stampedReference.getReference()+"\t"+stampedReference.getStamp());
//true Book(id=1, bookName=javaBook) 3
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
private int id;
private String bookName;
}
多线程下发生的问题
public class ABCDemo {
static AtomicInteger atomicInteger=new AtomicInteger(100);
public static void main(String[] args) {
new Thread(()->{
atomicInteger.compareAndSet(100,101);
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
atomicInteger.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(atomicInteger.compareAndSet(100,2022)+"\t"+atomicInteger.get());
},"t2").start();
}
}
//true 2022
多线程解决ABA
static AtomicInteger atomicInteger=new AtomicInteger(100);
static AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
new Thread(()->{
int state=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t首次版本号为"+stampedReference.getStamp());
//t3 首次版本号为1
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
stampedReference.compareAndSet
(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t二次版本号为"+stampedReference.getStamp());
//t3 二次版本号为2
stampedReference.compareAndSet
(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t三次版本号为"+stampedReference.getStamp());
//t3 三次版本号为3
},"t3").start();
new Thread(()->{
int state=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t首次版本号为"+state);
//t4 首次版本号为1
//等待t3发生过ABA
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
boolean flag = stampedReference.compareAndSet
(100, 2022, state, state + 1);
System.out.println(flag+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
//false 100 3
},"t4").start();
}