深入理解CAS

    科技2024-10-26  13

    CAS(compare-and-swap) ,保证数据的原子性,是硬件对于并发操作共享数据的支持 CAS不同于Synchronized,Synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起;而CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

    CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。 当且仅当V==A时,V = B,否则,不进行任何操作 看看它的源码

    public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }

    Unsafe 类

    Unsafe 类方法都是本地方法,因为Java无法直接和操作系统打交道,但是C++可以,所以提供了这个通道来间接操作

    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); public native Object getObjectVolatile(Object var1, long var2); public native void putObjectVolatile(Object var1, long var2, Object var4); public native int getIntVolatile(Object var1, long var2);

    再看看 getAndIncrement 操作,类似Java的 i++ 有三个参数,第一个是当前对象 第二个是值的偏移量,可以看成是值的内存地址 第三个就是自增1

    public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); //取出指定内存地址的值 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //相等,内存地址值加1 return var5; }

    因为cas是直接操作内存,所以效率极高

    CAS的缺陷

    1.著名的 ABA 问题 2. 只能保证一个共享变量的原子操作 3. 循环时间长开销大

    1、ABA 问题 CAS算法实现的一个重要前提是:取出内存中某时刻的数据,而在下一时刻比较并替换,那么在这个时间差中会导致数据的变化。

    简单举例: 线程 1 从内存位置V中取出A。 线程 2 从位置V中取出A。 线程 2 进行了一些操作,将B写入位置V。 线程 2 将A再次写入位置V。 线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。 尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。

    从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    public boolean compareAndSet( V expectedReference,//预期引用 V newReference,//更新后的引用 int expectedStamp, //预期标志 int newStamp //更新后的标志 ) import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicStampedReference; public class Test06 { public static void main(String[] args) { //原子引用,带 版本号的原子操作 乐观锁 AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1); new Thread(()->{ int stamp = atomicStampedReference.getStamp();//得到当前的版本号 System.out.println("A -version "+stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } boolean compareAndSet = atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); System.out.println("A IS "+compareAndSet); System.out.println("A -version "+atomicStampedReference.getStamp()); compareAndSet = atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); System.out.println("A IS "+compareAndSet); System.out.println("A -version "+atomicStampedReference.getStamp()); },"a").start(); new Thread(()->{ int stamp = atomicStampedReference.getStamp();//得到当前的版本号 System.out.println("B -version "+stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } boolean compareAndSet = atomicStampedReference.compareAndSet(1, 7, stamp, stamp + 1); System.out.println("B IS "+compareAndSet); System.out.println("B -version "+atomicStampedReference.getStamp()); },"b").start(); } }

    2、只能保证一个共享变量的原子操作

    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

    3、循环时间长开销大

    自旋锁,自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

    int var5; do { var5 = this.getIntVolatile(var1, var2); //比较当前工作内存中的值和主内存中的值,如果这个值是期望的,就执行操做,如果不是,就一直循环 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
    Processed: 0.040, SQL: 8