CAS面试题

1.讲一讲AtomicInteger类,为什么要用CAS而不是synchronized?
1.1 CAS(全称为compare-And-Swap)是什么?
它是一条CPU并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新值,这个过程是原子的 。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

// 5是初始值
AtomicInteger atomicInteger=new AtomicInteger(5);
//5是期望值,如果现在主内存里的值也是5就改为2019
System.out.println(atomicInteger.compareAndSet(5, 2019)+"==update=="+atomicInteger.get());
5是期望值,如果现在主内存里的值也是5就改为1024
 System.out.println(atomicInteger.compareAndSet(5, 1024)+"==update=="+atomicInteger.get());

执行结果为:

1.2 为什么AtomicInteger不加synchronized能实现原子性?
示例volatile不保证原子性,而AtomicInteger能实现原子性。
volatile不保证原子性如下:

public class test{
  public static void main(String[] args) throws InterruptedException {
        MyData myData=new MyData();
		for(int i=1;i<=20;i++){
		    new Thread(()->{
                for(int k=1;k<=1000;k++){
                    myData.add();
                }
            },String.valueOf(i)).start();
        }
		//需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值是多少
        //activeCount()>2为什么是大于2因为默认两个线程,一是main线程二是gc线程
        //大于2时让出cpu让上面20个线程执行
		while (Thread.activeCount()>2){
		    Thread.yield();
		    log.info("Thread.activeCount()=={}",Thread.activeCount());
        }
        System.out.println(Thread.currentThread().getName()+"finally i 
        value"+myData.num);
	}    
}

class MyData{
        volatile int num;
        public void add(){
          num++;
       }
   }

执行结果为,期望得到20000,实际得到的值是19713:

 用AtomicInteger保证原子性:

public class test{
   public static void main(String[] args) throws InterruptedException {
        MyData myData=new MyData();
		for(int i=1;i<=20;i++){
		    new Thread(()->{
                for(int k=1;k<=1000;k++){
                     myData.add();
                    myData.addAtomic();
                }
            },String.valueOf(i)).start();
        }
		//需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值是多少
        //activeCount()>2为什么是大于2因为默认两个线程,一是main线程二是gc线程
        //大于2时让出cpu让上面20个线程执行
		while (Thread.activeCount()>2){
		    Thread.yield();
		    log.info("Thread.activeCount()=={}",Thread.activeCount());
        }
        System.out.println(Thread.currentThread().getName()+"int type  value"+myData.num);
        System.out.println(Thread.currentThread().getName()+"atomic type  value"+myData.atomicInteger);
	}
}

class MyData{
    volatile int num;
    public void add(){
        num++;
    }
    AtomicInteger atomicInteger=new AtomicInteger();
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
}

执行结果为 :用AtomicInteger加1的结果是20000是正确的

16:10:07.575 [main] INFO com.hy.controller.ResController - Thread.activeCount()==18
main  int type  value19880
main  atomic type  value20000
AtomicInteger执行过程源码:
 AtomicInteger atomicInteger=new AtomicInteger();
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }

// getAndIncrement方法 this是当前对象valueOffset是内存偏移量,加1
 public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
 }

//Unsafe类的getAndAddInt方法,该Unsafe类是在jdk里的rt.jar包

   public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //var1对象里的var2这个内存偏移量的值 
            var5 = this.getIntVolatile(var1, var2);         
            //var1 AtomicInteger 对象本身
           //var2 该对象值得引用地址
          //var4 需要变动的数量 +1
         //var5 是用var1,var2找出的主内存中真实的值
        //用该 对象当前的值与var5比较
        //如果相同,更新var5+var4并且返回true
       //如果不同,继续取值然后 再比较,直到更新完成
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4))的执行流程:
 假设线程A和线程B两个线程同时执行getAndAddlnt操作(分别跑在不同CPU上) 
1 AtomicInteger里 面的value原始值为3,即主内存中AtomicInteger的value为3,根据JvM模型,线程A和线程B各自持有一  份值为3的value的副本分别到各自的工作内存。 
2 线程A通过getlIntVolatile(var1, var2)拿到value值3, 这时线程A被挂起。
3 线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法,比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。 4 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
5 线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。 


AtomicInteger 源码:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 创建Unsafe 对象
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
               //得到Unsafe 的内存偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //getAndIncrement方法里加1就是对value对1,因为是volatile 所以对其它线程可见
    private volatile int value;
}

为什么AtomicInteger不加synchronized能实现原子性?答案:
1.Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要 通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。注意,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
2.变量valueOffset表示该变量值在内存中偏移地址,因为Unsafe就是根据内存偏移地址获取数据的

// getAndIncrement方法 this是当前对象valueOffset是内存偏移量,加1
 public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
 }

3.变量value用volatile修饰,保证了多线程之间的可见性
 DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排序。
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能还没有完成初始化,instance=new SingletonDemo();可以分为以下3步完成
memory=allocate()  //1分配对象内存空间
instance(memory);  //2初始化对象
instance=memory; //3设置instance指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,可能会指令重排序如下:
memory=allocate()  //1分配对象内存空间
instance=memory;  //3设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成
instance(memory);  //2初始化对象
指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

以下代码是DCL(双端检锁):


class SingletonDemo{
    private SingletonDemo(){
        System.out.println("我是一个构造函数");
    }
    private static SingletonDemo instance=null;
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

禁止指令重排加volatile:


class SingletonDemo{
    private SingletonDemo(){
        System.out.println("我是一个构造函数");
    }
    private static volatile SingletonDemo instance=null;
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }
}
CAS缺点:
循环时间长,开销大
只能保证一个共享变量的原子操作。
会导致ABA问题,所谓ABA就是线程A把自己工作内存里的值改成别的值同步到主内存,然后又改回原来的值同步到主内存,过后线程B拿自己工作内存的值和主内存的值比较是一样然后更改。线程B并不知道A中间已经改过一次。用 AtomicStampedReference的时间戳版本号解决ABA问题,就是版本号比较,类似于Svn
public class ABADemo {
    static AtomicReference<Integer> atomicReference=new AtomicReference<Integer>(100);
    static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<Integer>(100,1);

    public static void main(String[] args) {
        System.out.println("=======以下是ABA问题的产生=======");
        new Thread(new Runnable() {
            public void run() {
                atomicReference.compareAndSet(100,101);
                atomicReference.compareAndSet(101,100);
            }
        },"t1").start();

        new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("更改是否成功="+atomicReference.compareAndSet(100,2019)+"=="+atomicReference.get());
            }
        },"t2").start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("=======以下是ABA问题的解决=======");
        new Thread(new Runnable() {
            public void run() {
                int stamp=atomicStampedReference.getStamp(); //版本号
                System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stamp);
                try {
                    Thread.sleep(1);  //为了让t4拿到 为1的版本号
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
                System.out.println(Thread.currentThread().getName()+"第二次版本号:"+atomicStampedReference.getStamp());
                atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName()+"第三次版本号:"+atomicStampedReference.getStamp());
            }
        },"t3").start();


        new Thread(new Runnable() {
            public void run() {
                int stamp=atomicStampedReference.getStamp(); //版本号
                System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stamp);
                try {
                    Thread.sleep(3); //为了让t3执行完修改操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean b = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName()+"修改是否成功:"+b+"版本号为:"+atomicStampedReference.getStamp());
                System.out.println(Thread.currentThread().getName()+"值为:"+atomicStampedReference.getReference());

            }
        },"t4").start();
    }
}

自旋锁:
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU 
自己实现一个自旋锁:
 

class MyData{ 
    AtomicReference<Thread> atomicReference=new AtomicReference<>();
    public void myLock(){
        Thread thread=Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"===come in");
        //先t1 期望值是null,确实是null返回true,把值设置为自己的thread
        //然后t2进来 期望值是null,这时候已经被t1改了,所以返回false,然后一直while循环
        //直到t1 执行myUnlock()方法把值改为null.这时候返回true
        while (!atomicReference.compareAndSet(null,thread)){
        }
    }

    public void myUnlock(){
        Thread thread=Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"===invoke myUnlock");
    }
    public static void main(String[] args) {
        MyData myData=new MyData();
        new Thread(()->{
            myData.myLock();
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.myUnlock();
        },"t1").start();

        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            myData.myLock();
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.myUnlock();
        },"t2").start();
    }
}

执行结果:

t1===come in
t2===come in
t1===invoke myUnlock
t2===invoke myUnlock


 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值