简单分析CAS机制

目录

一、CAS是什么?

二、 CAS与synchronized

三、CAS能解决什么问题

四、CAS在java中的应用

五、CAS缺点

1、ABA问题

2、长时间自旋非常消耗资源

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


一、CAS是什么?

        cas是比较并交换 compareAndSwap(compareAndSwapInt),它的功能是判断内存某个位置的值(主内存中的值)是否为预期值(工作内存中变量副本的值),如果是则更改为新的值,这个过程是原子的。cas是一条cpu的原子指令,不会造成所谓的数据不一致问题。

        比较当前工作内存中的值和主内存中的值,是否一致,如果一致则执行规定操作,否则重新读取主内存值,继续比较直到主内存和工作内存中的值一致为止。

二、 CAS与synchronized

 synchronized关键字可以保证同步,但存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

三、CAS能解决什么问题

        上面我们了解了cas是什么了,那么它能解决什么问题呢?它可以解决多线程并发安全的问题,以前我们对一些多线程操作的代码都是使用synchronize关键字,来保证线程安全的问题;现在我们将cas放入到多线程环境里我们看一下它是怎么解决的,我们假设有A、B两个线程同时执行一个int值value自增的代码,并且同时获取了当前的value,我们还要假设线程B比A快了那么0.00000001s,所以B先执行,线程B执行了cas操作之后,发现当前值和预期值相符,就执行了自增操作,此时这个value = value + 1;然后A开始执行,A也执行了cas操作,但是此时value的值和它当时取到的值已经不一样了,所以此次操作失败,重新取值然后比较成功,然后将value值更新,这样两个线程进入,value值自增了两次,符合我们的预期。

四、CAS在java中的应用

        java中的Atomic系列就是使用cas实现的,下面我们就用AtomicInteger类看一下java是怎么实现的吧。

        cas有三个操作数,内存值V(var5),旧的预期值A(var1,var2),要修改的更新值B(var5+var4)。当且仅当内存值(主内存)V和预期值(工作内存)A一致时,将内存V修改为B,否则什么都不做(自旋)。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L; 
// setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	//变量value用volatile,保证了多线程之间的可见性
    private volatile int value;

	public final int getAndIncrement() {
        //变量valueOffset表示该变量值在内存中的偏移地址,
       //Unsafe就是根据内存偏移地址获取数据的
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
	/**
	var1 AtomicInteger
	var2 该对象值的引用地址
	var4 需要变动的数量
	var5 是用过var1 var2找出主内存中真实的值。
	用该对象当前的值与var5比较
	如果相同,更新var4+var5并且返回true
	如果不同,继续取值然后再比较,直到更新完成
	**/
    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));

        return var5;
    }
}

        unSafe是jdk中rt.jar包自带的类,是CAS核心类,由于java无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe存在于包sun.misc包中,内部方法操作可以像C一样直接操作内存,因为java中cas操作的执行依赖于Unsafe类的方法。

五、CAS缺点

1、ABA问题

        ABA问题产生是CAS只管头和尾,也就是说开始取主内存的值和写回主内存时,与主内存的值一样就替换,这样就产生了ABA问题。具体的如多个线程1、2读取到主内存值A到工作内存后,其中1线程在写回主内存前,2线程存在时间差(1线程执行时间10秒,2线程执行时间2秒),导致2线程多次修改主内存值A为B,但最终还是修改为原始主内存值A,那么线程1的操作仍然成功。尽管线程1操作成功,但是不代表这个过程是没问题的。

解决ABA 问题,引入带有时间戳的对象引用 AtomicStampReference原子引用! 对应的思想:乐观锁!

类似版本号机制,这里对象内部不仅维护了对象值,还维护了一个时间戳。当对应的值被修改时,同时更新时间戳。当CAS进行比较时,不仅要比较对象值,也要比较时间戳是否满足期望值,两个都满足,才会进行更新操作。

package com.study.cas;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * casABA问题
 */
public class AtomicReferenceDemo2 {

    static AtomicReference money = new AtomicReference<>(10);
    static AtomicStampedReference moneySta = new AtomicStampedReference<>(10, 1);

    public static void main(String[] args) {
        Integer m = money.get();
        new Thread(() -> {
            money.compareAndSet(money.get(), money.get() + 10);
            money.compareAndSet(money.get(), money.get() + 10);
            boolean a = money.compareAndSet(money.get(), money.get() - 20);
            System.out.println(Thread.currentThread().getName() + "结果:" + a +
                    "当前money= " + money.get());
        }, "A").start();
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = money.compareAndSet(10, m + 30);
            System.out.println(Thread.currentThread().getName() + "结果:" + b +
                    "当前money= " + money.get());
        }, "B").start();
        //--------------------------以下是ABA问题解决----------
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "读取到初始值 = " + moneySta.getReference());
            moneySta.compareAndSet(10, 20, moneySta.getStamp(), moneySta.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第1次版本号:" + moneySta.getStamp() + " 修改后的值:" + moneySta.getReference());
            moneySta.compareAndSet(20, 30, moneySta.getStamp(), moneySta.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第2次版本号:" + moneySta.getStamp() + " 修改后的值:" + moneySta.getReference());
            boolean a = moneySta.compareAndSet(30, 10, moneySta.getStamp(), moneySta.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第3次版本号:" + moneySta.getStamp() + " 修改后的值:" + moneySta.getReference());
        }, "C").start();
        new Thread(() -> {
            int stamp = moneySta.getStamp();
            try {
                //暂停3秒保证C线程完成一次ABA操作
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "读取到初始值 = " + stamp);
            boolean b = moneySta.compareAndSet(10, 49, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "当前版本号:" + stamp + " 修改成功否:" + b);
            System.out.println(Thread.currentThread().getName() + "当前实际最新版本号:" + moneySta.getStamp() + " 实际最新值:" + moneySta.getReference());
        }, "D").start();

    }
}


  • 运行结果
A结果:true当前money= 10
B结果:true当前money= 40
C读取到初始值 = 10
C第1次版本号:2 修改后的值:20
C第2次版本号:3 修改后的值:30
C第3次版本号:4 修改后的值:10
D读取到初始值 = 1
D当前版本号:1 修改成功否:false
D当前实际最新版本号:4 实际最新值:10

2、长时间自旋非常消耗资源

        先说一下什么叫自旋,自旋就是cas的一个操作周期,如果一个线程特别倒霉,每次获取的值都被其他线程的修改了,那么它就会一直进行自旋比较,直到成功为止,在这个过程中cpu的开销十分的大,所以要尽量避免。

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

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

你可能感兴趣的:(java深入理解,java)