7、CAS

目录

1、CAS是什么

1.1 CAS基本知识

1.2 CAS基本思想

2、对原子类中使用的CAS进行分析

2.1 CAS demo 代码

2.2 源码分析

3、原子类 

3.1 有那些属性(用AtomicInteger分析)

3.2 Unsafe类解析 

4、使用原子类自定义原子引用

5、利用CAS思想,实现自旋锁SpinLockDemo

6、CAS缺点

6.1 循环时间长CPU开销会变大

6.2 ABA问题

6.2.1 什么是ABA问题

6.2.2 解决:加版本号、流水


1、CAS是什么

1.1 CAS基本知识

        Compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用的一种技术。它包含三个操作数——内存位置、预期原值、更新值

1.2 CAS基本思想

        CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B

        当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

        重试的行为称为——自旋

过程解析:

        1、线程A、B 同时读取到了主内存的共享变量number(5)并复制到各自线程工作内存中成为共享变量副本number'(5)

        2、线程A、B同时计算出结果number''(6)

        3、到写回主内存中时只能由一个先执行(这里为什么先不用纠结,后面会说——靠的是CPU原语级别的锁,硬件来保证原子性)

        4、线程A执行,线程B挂起;线程A进行共享变量number'(5)与主内存number(5)比较,发现一致修改内存值中的number=6

        5、线程B执行时,进行比较线程B中的number'(5)与主内存中的number(6)对比发现不一致,然后需要自旋重新走一遍线程B的方法

2、对原子类中使用的CAS进行分析

2.1 CAS demo 代码

        多线程情况下使用原子类可以保证线程安全

public class CasDemo {
    public static void main(String[] args){
        AtomicInteger atomicInteger = new AtomicInteger(5);

        System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t"+atomicInteger.get());
    }
}

//true	2020
//false	2020
public class CasDemo {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 10 ; i++){
            new Thread(() -> {
                for(int j = 1 ; j <= 500; j++){
                    atomicInteger.incrementAndGet();
                }
            },String.valueOf(i)).start();
        }
        // 等待所有线程执行完毕,  这里还可以使用CountDownLatch
        Thread.sleep(2000L);
        System.out.println(atomicInteger.get());
    }
}
// 5000

上面代码的优化

public class CasDemo {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    /**  线程数 */
    static Integer THREAD_NUM = 10;
    /** 线程控制 */
    static CountDownLatch downLatch = new CountDownLatch(THREAD_NUM);

    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < THREAD_NUM ; i++){
            new Thread(() -> {
                try{
                    for(int j = 1 ; j <= 500; j++){
                        atomicInteger.incrementAndGet();
                    }
                }finally {
                    downLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        // 等待所有线程执行完毕
        downLatch.await();
        System.out.println(atomicInteger.get());
    }
}

// 5000

2.2 源码分析

        不管是调用了什compareAndSet 还是 incrementAndGet() 等方法;底层都调用的是compareAndSwapXxx

//compareAndSet
//发现它调用了Unsafe类
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}


//compareAndSwapInt
//发现它调用了native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这三个方法是类似的
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/var6:表示需要修改为的新值

3、原子类 

3.1 有那些属性(用AtomicInteger分析)

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();
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;//保证变量修改后多线程之间的可见性
}

1、unsafe:保证数据的原子性

2、valueOffset:该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据

3、value:保存的数据 使用volatile修饰,保证可见性

3.2 Unsafe类解析 

        CAS这个理念 ,落地就是Unsafe

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

        注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务 。

 

 OpenJDK源码中查看Unsafe.java 源码地址:Unsage.java位置

1、do-while 体现了自旋的思想

2、如果compareAndSwapInt 为true设置成后取反为false退出循环;假如返回的是false表示设置失败,取反true继续循环

 以下内容可以看一看,不需要懂!!!!

compareAndSwapInt方法被native关键字修饰,所以是一个本地方法,该方法可以从unsafe.cpp中看到,源码地址:unsafe.cpp位置

 UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv* env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
      UnsafeWrapper("Unsafe_CompareAndSwapInt");
      oop p = JNIHandles::resolve(obj);
    // 先想办法拿到变量value在内存中的地址,根据偏移量valueOffset,计算 value 的地址
      jint* addr = (jint* ) index_oop_from_field_offset_long(p, offset);
    // 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是要交换的值,e是要比较的值
    //cas成功,返回期望值e,等于e,此方法返回true;
    //cas失败,返回内存中的value值,不等于e,此方法返回false
      return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
    UNSAFE_END
    //-------------核心(Atomic::cmpxchg(x, addr, e)) == e;
    //JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用cmpxchg指令比较并更新变量值(原子性)
// 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
unsigned Atomic::cmpxchg(unsigned int exchange_value,volatile unsigned int* dest, unsigned int compare_value) {
    assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
    //根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数
    return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value);
}

不同的操作系统下会调用不同的compxchg重载函数,例如win10

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  //判断是否是多核CPU
  int mp = os::is_MP();
  __asm {
    //三个move指令表示的是将后面的值移动到前面的寄存器上
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    //CPU原语级别,CPU触发
    LOCK_IF_MP(mp)
    //比较并交换指令
    //cmpxchg: 即“比较并交换”指令
    //dword: 全称是 double word 表示两个字,一共四个字节
    //ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元 
    //将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值进行对比,
    //如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中
    cmpxchg dword ptr [edx], ecx
  }
}

总结(需背)

        1、unsafe类提供了硬件级别的原子性操作,实现方式是基于硬件平台的汇编指令,在intel的CPIU中,使用的汇编指令是cmpxchg指令

        2、核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设置为新的N(swap)。如果不相等自旋再来。

4、使用原子类自定义原子引用

 案例:

public class AtomicReferenceDemo {

    public static void main(String[] args) {
        User zs = new User("张三",18);
        User ls = new User("李四",25);

        // 将类型丢入泛型即可
        AtomicReference<User> atomicReferenceUser = new AtomicReference<>();
        // 将这个原子类设置为张三
        atomicReferenceUser.set(zs);
        // 张三换位李四
        System.out.println(atomicReferenceUser.compareAndSet(zs,ls)+"\t"+atomicReferenceUser.get().toString());
        // true    User(userName=李四, age=25)
        System.out.println(atomicReferenceUser.compareAndSet(zs,ls)+"\t"+atomicReferenceUser.get().toString());
        // false   User(userName=李四, age=25)
    }
}


@Data
@AllArgsConstructor
class User {
    String userName;
    int age;
}

5、利用CAS思想,实现自旋锁SpinLockDemo

题目:实现一个自旋锁

自旋锁好处:循环比较获取没有类似wait的阻塞。

通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。

public class SpinLockDemo {

    AtomicReference<Thread> lock = new AtomicReference();

    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " 争抢锁");
        // 用这个循环实现自旋;如果是空的表示没有线程持有该锁,那把当前thread放进去
        while (!lock.compareAndSet(null,thread)){

        }
        System.out.println(thread.getName() + " 持有锁");
    }

    public void unlock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " 准备释放锁");
        // 把当前线程踢出去,置为null
        boolean compareAndSet = lock.compareAndSet(thread, null);
        System.out.println(compareAndSet ? thread.getName() + " 释放锁成功" : thread.getName() + " 释放锁失败");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.lock();
            try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.unlock();
        },"A").start();

        //暂停一会儿线程,保证A线程先于B线程启动并完成
        try { TimeUnit.MILLISECONDS.sleep( 500); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            spinLockDemo.lock();//B  -----come in  B只是尝试去抢锁,但是一直在自旋。

            spinLockDemo.unlock();//A结束后 B立马抢到锁,然后马上结束了
        },"B").start();
    }
}

//A 争抢锁
//A 持有锁
//B 争抢锁
//A 准备释放锁
//A 释放锁成功
//B 持有锁
//B 准备释放锁
//B 释放锁成功

6、CAS缺点

6.1 循环时间长CPU开销会变大

如果CAS一直失败,会一直尝试,可以回给CPU带来很大的开销。

6.2 ABA问题

6.2.1 什么是ABA问题

        线程A先将内存中的数据取出,这个时候线程B也将内存中的数据拿出;线程A做了两步操作,第一步将内存的数据改成了其他的数据,第二步操作又将数据改会了一开始内存的值;这个时候线程B再来比较的时候发现自己的值与预期值一致,就进行了改变!

这个事情可以用现实情况去比喻:

        1、先挪用公款的钱去理财,然后等公司要钱的时候,再将钱换回去!这样看似没有什么问题,但是其实已经犯了罪;所以银行每一笔钱的来往都有流水。

        2、隔壁老王案例

6.2.2 解决:加版本号、流水

        AtomicStampedReference版本号 (注意区分前面的Class AtomicReference)

// 相关API
AtomicStampedReference(V initialRef, int initialStamp)
创建一个新的 AtomicStampedReference与给定的初始值。

 public boolean weakCompareAndSet(V expectedReference,//旧值
                                 V newReference,//新值
                                 int expectedStamp,//旧版本号
                                 int newStamp)//新版本号
以原子方式设置该引用和邮票给定的更新值的值,如果当前的参考是==至预期的参考,并且当前标志等于预期标志。
May fail spuriously and does not provide ordering guarantees ,所以只是很少适合替代compareAndSet 。

参数
expectedReference - 参考的预期值
newReference - 参考的新值
expectedStamp - 邮票的预期值
newStamp - 邮票的新值
结果
true如果成功
  //基本情况
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    class Book{
        private  int id;
        private String bookName;
    }

    public class AtomicStampedDemo {
        public static void main(String[] args) {
            Book javaBook = new Book(1, "javaBook");
            AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook,1);
            System.out.println(stampedReference.getReference()+"\t"+stampedReference.getReference());
            Book mysqlBook = new Book(2, "mysqlBook");
            boolean b = stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
        }
    }
    //Book(id=1, bookName=javaBook)  Book(id=1, bookName=javaBook)
    //true  Book(id=2, bookName=mysqlBook)  2
  

   //ABA复现(单线程情况下)

    ```java
    public class AtomicStampedDemo {
        public static void main(String[] args) {
            Book javaBook = new Book(1, "javaBook");
            AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook,1);
            System.out.println(stampedReference.getReference()+"\t"+stampedReference.getReference());
            Book mysqlBook = new Book(2, "mysqlBook");
            boolean b;
                    b= stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
            b= stampedReference.compareAndSet(mysqlBook,javaBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
        }
    }
    //Book(id=1, bookName=javaBook)  Book(id=1, bookName=javaBook) --------
    //true  Book(id=2, bookName=mysqlBook)  2
    //true  Book(id=1, bookName=javaBook)  3  --------虽然1.3行内容是一样的,但是版本号不一样



   //ABA复现(多线程情况下)
    public class ABADemo{
        static AtomicInteger atomicInteger = new AtomicInteger(100);
        static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

        public static void main(String[] args){
            new Thread(() -> {
                atomicInteger.compareAndSet(100,101);
                atomicInteger.compareAndSet(101,100);//这里 中间就有人动过了,虽然值是不变的,假如不检查版本号,CAS就直接能成功了
            },"t1").start();

            new Thread(() -> {
                //暂停一会儿线程
                try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); };            
                System.out.println(atomicInteger.compareAndSet(100, 2022)+"\t"+atomicInteger.get());
            },"t2").start();
            
            //-------------------- true-2022

            //暂停一会儿线程,main彻底等待上面的ABA出现演示完成。
            try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); }

            System.out.println("============以下是ABA问题的解决=============================");

            new Thread(() -> {
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1-----------初始获得一样的版本号
                //暂停500毫秒,保证t4线程初始化拿到的版本号和我一样,
                try { TimeUnit.MILLISECONDS.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); }
                atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName()+"\t 2次版本号:"+atomicStampedReference.getStamp());
                atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName()+"\t 3次版本号:"+atomicStampedReference.getStamp());
            },"t3").start();

            new Thread(() -> {
                int stamp = atomicStampedReference.getStamp();//记录一开始的版本号,并且写死
                System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1------------初始获得一样的版本号
                //暂停1秒钟线程,等待上面的t3线程,发生了ABA问题
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);//这个还是初始的版本号,但是实际上版本号被T3修改了,所以肯定会失败
                System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
            },"t4").start();
        }
    }
    //t3 首次版本号:1
    //t4 首次版本号:1
    //t3 2次版本号:2
    //t3 3次版本号:3
    //false 100 3   -----因为版本号实际上已经被修改了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郭吱吱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值