对CAS事的理解及一些面试题

wuchangjian2021-11-03 12:07:07编程学习

CAS定义:

CAS的全称是Compare-And-Swap,它是CPU并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的
CAS操作包含三个操作数一内存位置 (V)、期望值(A) 和新值(B)
如果内存位置的值与期望值匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不作任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。(CAS在一些特殊情况下仅返回CAS是否成功,而不提取当前值) CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置的值,只告诉我这个位置现在的值即可。” 是一种不同于synchronized的乐观锁

案例:

用synchronized实现线程安全

public class CASDemo01 {

    volatile static int count = 0;

    public synchronized static void request() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(5);
        count++;
    }


    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }
        countDownLatch.await();
        long endTime = System.currentTimeMillis();

        System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ",count = " + count);
    }
}

结果: main, 耗时:6332,count = 1000

模拟CAS实现线程安全

public class SimulationCAS {

    volatile static int count = 0;

    public static void request() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(5);
        int expectCount;
        while (!compareAndSwap(expectCount = getCount(), expectCount + 1)) {

        }
    }


    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }
        countDownLatch.await();
        long endTime = System.currentTimeMillis();

        System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ",count = " + count);
    }

    public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
        if (getCount() == expectCount) {
            count = newCount;
            return true;
        }
        return false;
    }

    public static int getCount() {
        return count;
    }
}

结构:main, 耗时:205,count = 1000

Q:怎么使用JDK提供的CAS支持?

A: java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中, 声明如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

参数var1:表示要操作的对象
参数var2:表示要操作对象中属性地址的偏移量
参数var4;表示需要修改数据的期望的值
参数var5:表示需要修改为的新值

Q: CAS实现原理是什么?

A: CAS通过调用JNI的代码实现,JNI: java Native Interface, 允许java调用其它语言。而compareAndSwapxxx系列的方法就是借助“C语言”来调用cpu底层指令实现的。以常用的Intel x86平台来说,最终映射到的cpu的指令为cmpxchg,这是一个原子指令,cpu执行此命令时,实现比较并替换的操作!

Q:现代计算机动不动就上百核心,cmpxchg怎么保证多核心下的线程安全?

A: 系统底层进行CAS操作的时候,会判断当前系统是否为多核心系统,如果是就给“总线”加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性是平台级别的!
在这里插入图片描述

Q:CAS的优缺点

A:

优点:
不加锁,性能比较好

缺点:
1.do while循环时间长的话开销大(如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
2.只能保证一个共享变量的原子性
当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作,但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
3.ABA问问题

// ursafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
	int var5;
	do {
		var5 = this.getIntVolatile(var1, var2);
	}while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
    return var5;
}

Q:如何解决ABA问题?

A:解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它的版本号,CAS操作时都去对比此版本号。java中ABA解决方法(AtomicStampedReference)AtomicStampedReference主要包含一个对象引用及一个可以自动更新的整数“stamp?"的pair对象来解决ABA问题。

AtomicStampedReference里面维护了一个Pair对象, Pair里面封装了引用reference和时间戳stamp,就是简单的一层封装。
在这里插入图片描述

   public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference && //期望引用和当前引用一致
            expectedStamp == current.stamp &&  // 期望版本和当前版本一致
            ((newReference == current.reference &&  //如果当前pair的引用或者时间戳已经和新的一样,就不会走 `||`后面的一部分了
              newStamp == current.stamp) ||			//就是已经设置过,不重复设置	
             casPair(current, Pair.of(newReference, newStamp)));
    }
    ```

在这里插入图片描述

在这里插入图片描述
ABA问及解决方法demo

public class ABADemo {
    private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        System.out.println("=========================下面是ABA问题======================================");
        new Thread(()->{
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();

        new Thread(()->{
            //先暂停1秒 保证完成ABA
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(atomicReference.compareAndSet(100, 2019)+"\t"+atomicReference.get());
        },"t2").start();
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("=========================下面是解决ABA问题的方法======================================");
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
            //暂停1秒钟t3线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第2次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第3次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
        },"t3").start();

        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
            //保证线程3完成1次ABA
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t 修改成功否"+result+"\t最新版本号"+stampedReference.getStamp());
            System.out.println("最新的值\t"+stampedReference.getReference());
        },"t4").start();
    }
}

=========================下面是ABA问题======================================
true	2019
=========================下面是解决ABA问题的方法======================================
t4	 第1次版本号1	值是100
t3	 第1次版本号1	值是100
t3	 第2次版本号2	值是101
t3	 第3次版本号3	值是100
t4	 修改成功否false	最新版本号3
最新的值	100
返回列表

上一篇:全局使用scss变量

下一篇:事务

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。