CAS介绍
compare and swap的缩写,中文翻译就是比较并交换,实现并发算法时常用的一种技术,他包含三个操作:内存位置,预期原值,更新值。执行CAS操作时,将内存位置的值和预期原值进行比较,如果相匹配,那么处理器会自动将该位置的值更新为新值,如果不匹配,处理器不做任务操作,多线程同时执行CAS操作只有一个会成功。
CAS操作流程图如图:CAS操作流程图所示
CAS有三个操作数,位置内存值V,旧的预期值A,要修改的更新值B。当且仅有当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或者重新来,当他重新来或者重试的这种行为就是自旋
硬件级别保证CAS
CAS是JDK提供的非阻塞原子性操作,他通过硬件保证了比较-更新的原子性它是非阻塞的且自身具有原子性,也就是说CAS效率更高,并且更加可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
CAS实现原理Unsafe
- 是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地〈native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像c的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务 - 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
- 变量value用volatile修饰,保证了多线程之间的内存可见性。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
原子引用类
除了基本数据类型的Atomic类之外(例如常见的:AtomicInteger,AtomicBoolean等),JDK还提供了对复杂数据类型的原子包装类:AtomicReference类
public class CASTest {
private final static Logger log = LoggerFactory.getLogger(CASTest.class);
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.addAndGet(1);
log.info("执行结果: {},最新值:{}", atomicInteger.compareAndSet(5, 2022), atomicInteger.get());
log.info("执行结果: {},最新值:{}", atomicInteger.compareAndSet(5, 2022), atomicInteger.get());
}
}
自旋锁
CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
手写实现自旋锁案例如下:
public class SpinLockDemo {
private final static Logger log= LoggerFactory.getLogger(SpinLockDemo.class);
private AtomicReference<Thread> threadAtomicReference=new AtomicReference<>();
public void lock(){
Thread thread=Thread.currentThread();
while(!threadAtomicReference.compareAndSet(null,thread)){
}
log.info("线程{}---------Lock",thread.getName());
}
public void unlock(){
Thread thread=Thread.currentThread();
threadAtomicReference.compareAndSet(thread,null);
log.info("线程{}---------Unlock",thread.getName());
}
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();
TimeUnit.MILLISECONDS.sleep(200);
new Thread(()->{
spinLockDemo.lock();
spinLockDemo.unlock();
},"B").start();
}
}
CAS自旋锁缺点
-
循环时间过长,CPU开销较大
-
引起ABA问题
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B.然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
ABA问题演示案例如下:
public class ABATest {
private final static Logger log= LoggerFactory.getLogger(ABATest.class);
public static void main(String[] args) {
ABAProblemDemonstration();
}
public static void ABAProblemDemonstration(){
Book book1=new Book("Java",18);
Book book2=new Book("MySQL",20);
AtomicReference<Book> book2AtomicReference=new AtomicReference<>(book1);
new Thread(()->{
log.info("线程{}修改值成功:{},当前的值为:{}",
Thread.currentThread().getName(),
book2AtomicReference.compareAndSet(book1,book2),
book2AtomicReference.get().toString()
);
log.info("线程{}修改值成功:{},当前的值为:{}",
Thread.currentThread().getName(),
book2AtomicReference.compareAndSet(book2,book1),
book2AtomicReference.get().toString()
);
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("线程{}修改值成功:{},当前的值为:{}",
Thread.currentThread().getName(),
book2AtomicReference.compareAndSet(book1,book2),
book2AtomicReference.get().toString()
);
},"B").start();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
class Book{
private String name;
private Integer price;
}
上述例子的运行截图如图:ABA问题运行截图所示
ABA问题解决方案
JDK提供了另外一种原子引用类:AtomicStampedReference,上述的原子引用类AtomicReference类是对结果进行比对,当预期值和旧值相互匹配时则进行修改,即使其中的值经过了变换(A到B,B到A)也一样会成功。但是AtomicStampedReference类不仅通过新旧值比对,还加入了邮戳的形式进行验证
public class ABATest {
private final static Logger log= LoggerFactory.getLogger(ABATest.class);
public static void main(String[] args) {
ABAProblemAvoidance();
}
public static void ABAProblemAvoidance(){
Book book1=new Book("Java",18);
Book book2=new Book("MySQL",20);
AtomicStampedReference<Book> book2AtomicStampedReference=new AtomicStampedReference<>(book1,1);
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int expectedStamp=book2AtomicStampedReference.getStamp();
int newStamp=expectedStamp+1;
log.info("\n旧值为:{},\n旧邮戳为:{},\n期望邮戳为:{},\n线程{}修改值成功:{},\n新值为:{},\n新邮戳为:{}",
book2AtomicStampedReference.getReference().toString(),
book2AtomicStampedReference.getStamp(),
expectedStamp,
Thread.currentThread().getName(),
book2AtomicStampedReference.compareAndSet(book1,book2,expectedStamp,newStamp),
book2AtomicStampedReference.getReference().toString(),
book2AtomicStampedReference.getStamp()
);
log.info("-------------------------------------------------------------------------------");
expectedStamp=book2AtomicStampedReference.getStamp();
log.info("\n旧值为:{},\n旧邮戳为:{},\n期望邮戳为:{},\n线程{}修改值成功:{},\n新值为:{},\n新邮戳为:{}",
book2AtomicStampedReference.getReference().toString(),
book2AtomicStampedReference.getStamp(),
expectedStamp,
Thread.currentThread().getName(),
book2AtomicStampedReference.compareAndSet(book2,book1,expectedStamp,newStamp),
book2AtomicStampedReference.getReference().toString(),
book2AtomicStampedReference.getStamp()
);
log.info("-------------------------------------------------------------------------------");
},"A").start();
new Thread(()->{
//获取当前的邮戳作为预期值
int expectedStamp=book2AtomicStampedReference.getStamp();
int newStamp=expectedStamp+1;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("\n旧值为:{},\n旧邮戳为:{},\n期望邮戳为:{},\n线程{}修改值成功:{},\n新值为:{},\n新邮戳为:{}",
book2AtomicStampedReference.getReference().toString(),
book2AtomicStampedReference.getStamp(),
expectedStamp,
Thread.currentThread().getName(),
book2AtomicStampedReference.compareAndSet(book1,book2,expectedStamp,newStamp),
book2AtomicStampedReference.getReference().toString(),
book2AtomicStampedReference.getStamp()
);
},"B").start();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
class Book{
private String name;
private Integer price;
}