带着BAT大厂的面试问题去理解
- 线程安全的实现方法有哪些?
- 什么是CAS?
- CAS使用示例,结合AtomicInteger给出示例?
- CAS会有哪些问题?
- 针对这这些问题,Java提供了哪几个解决的?
- AtomicInteger底层实现? CAS+volatile
- 请阐述你对Unsafe类的理解?
- 说说你对Java原子类的理解? 包含13个,4组分类,说说作用和使用场景。
- AtomicStampedReference是什么?
- AtomicStampedReference是怎么解决ABA的? 内部使用Pair来存储元素值及其版本号
- java中还有哪些类可以解决ABA的问题? AtomicMarkableReference
什么是CAS
CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。 简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。
(AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升)
CAS使用示例
java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。
/*
* CAS:Compare and swap [比较并交换]
* */
public class AtomicIntegerDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
//true 2019
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t"+atomicInteger.get());
//false 2019
System.out.println(atomicInteger.compareAndSet(5, 2222)+"\t"+atomicInteger.get());
}
}
关于unSafe.getAndIncrement()方法的分析
手写CAS锁
@Test
public void test() {
Thread aa = new Thread(() -> {
try {
Thread.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (!(atomicReference.compareAndSet(null,Thread.currentThread()))){
}
log.info("A跳出循环");
}, "AA");
aa.start();
Thread bb = new Thread(() -> {
while (!(atomicReference.compareAndSet(aa,Thread.currentThread()))){
log.info("正在CAS自旋");
}
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("B跳出循环");
}, "BB");
bb.start();
}
UnSafe类详解
我们了解到Java原子类是通过UnSafe类实现的,UnSafe类在JUC中CAS操作有很广泛的应用。
UnSafe类是CAS的核心类,由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作依赖于UnSafe类的方法
注意:UnSafe类中所有的方法都是 native 修饰的,也就是说 UnSafe 类中的方法都是直接调用操作底层资源执行响应的任务
先来看下这张图,对UnSafe类总体功能:
从源码中发现,内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)。
又从Unsafe类中发现,原子操作其实只支持下面三个方法。
public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
CAS的缺点
- ①. 循环时间长开销很大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
- ②. 只能保证一个共享变量的原子性
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
③. ABA问题的产生
因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
部分乐观锁的实现是通过版本号(version
)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1
操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。
从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
@Test
public void test2(){
Book javaBook = new Book(1,"Java设计模式");
AtomicStampedReference<Book> atomicStampedReference = new AtomicStampedReference<>(javaBook,1);
Book mysqlBook = new Book(2, "MySQL");
boolean b = atomicStampedReference.compareAndSet(javaBook, mysqlBook, 1, 2);
System.out.println(b);//true
}