CAS了解一下

Posted tianjx01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CAS了解一下相关的知识,希望对你有一定的参考价值。

今天咱们还是给自个充充电,就不大战秃头老了!等冲个差不多,电死!

今天我们就来聊一下CAS,这个玩意,太重要了,是并发包(JUC)的基础,没有它可以说是并发包简直就是废废的,说它之前咱们先讨论一下线程不安全的场景,并尝试解决一下!


目的:怎么让一个变量的快速到达200?

/**
 * @author: tianjx
 * @date: 2022/1/2 16:22
 * @description: 线程不安全问题!
 */
public class CASDemo04 

    // 类的成员变量
    static int data = 0;

    // main方法内代码
    public static void main(String[] args) 
       IntStream.range(0,5).forEach((i)->
           new Thread(()->
               IntStream.range(0,40).forEach((j)->
                   data++;
               );
           ).start();
       );
        try 
            TimeUnit.SECONDS.sleep(2);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(data);
    

哦?发现用多线程,竟然会造成结果不等于200!(假设A,B线程同时拿到data(100),A快一点,A现在工作内存中改成101,然后写到主内存,然后B开始,先在工作内存改成101,然后在写到主内存101,明明加了两次,但是结果确实101!)

啊!这可怎么办?那肯定就是加锁吧!

方法一:那…synchronized登场吧,我们常用的!

/**
 * @author: tianjx
 * @date: 2022/1/2 16:39
 * @description: synchronized 解决线程不安全
 */
public class CASDemo05 
    // 类的成员变量
    static int data = 0;

    // main方法内代码
    public static void main(String[] args) 
        IntStream.range(0,5).forEach((i)->
            new Thread(()->
                synchronized (CASDemo05.class)
                    IntStream.range(0,40).forEach((j)->
                        data++;
                    );
                
            ).start();
        );
        try 
            TimeUnit.SECONDS.sleep(2);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(data);
    

这个确实可以,但是我们都清楚synchronized是悲观锁,它会造成以下缺陷:

1、如果根据时间片来获取锁,加锁,释放锁,再加锁,在释放锁,那么就会造成频繁的上下文切换,线程一多,反而多线程的性能可能还不如单线程!

2、一个线程持有锁之后,所有的线程都会被阻塞或者挂起!这不是假多线程嘛!

(其实synchronized也做过很多的优化,刚刚开始可不是重量级锁哦!如果大家也想了解了解,请双击屏幕点个赞,并且关注不迷路哦!跪谢!)

方法二:lock(朋友莫慌,后面会介绍)

public class CASDemo06 
    // 类的成员变量
    static int data = 0;
    static Lock lock = new ReentrantLock();

    // main方法内代码
    public static void main(String[] args) 
        IntStream.range(0,5).forEach((i)->
            new Thread(()->
                IntStream.range(0,40).forEach((j)->
                    lock.lock();
                    data++;
                    lock.unlock();
                );
            ).start();
        );
        try 
            TimeUnit.SECONDS.sleep(2);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(data);
    

方法三:AtomicInteger(朋友莫慌,后面会介绍)

/**
 * @author: tianjx
 * @date: 2022/1/2 16:54
 * @description: Atomic
 */
public class CASDemo07 
    // 类的成员变量
    static AtomicInteger atomicInteger = new AtomicInteger(0);

    // main方法内代码
    public static void main(String[] args) 
        IntStream.range(0,5).forEach((i)->
            new Thread(()->
                IntStream.range(0,40).forEach((j)->
                    atomicInteger.incrementAndGet();
                );
            ).start();
        );
        try 
            TimeUnit.SECONDS.sleep(2);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(atomicInteger.get());
    

方法四:LongAdder(朋友莫慌,后面会介绍)

/**
 * @author: tianjx
 * @date: 2022/1/2 16:58
 * @description:
 */
public class CASDemo08 
    // 类的成员变量
    static LongAdder longAdder = new LongAdder();

    // main方法内代码
    public static void main(String[] args) 
        IntStream.range(0,5).forEach((i)->
            new Thread(()->
                IntStream.range(0,40).forEach((j)->
                    longAdder.increment();
                );
            ).start();
        );
        try 
            TimeUnit.SECONDS.sleep(2);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(longAdder);
    

  • Lock底层是用AQS+CAS来实现的,在高并发的场景下,比synchronized性能高的可不是一点半点
  • AtomicIntegerJUC并发包下的,底层实现也是CAS,相对于lock不断加锁,释放锁更加优雅!
  • LongAdder是JDK1.8之后新增的类,也是JUC并发包下的,不用说实现也是用了CAS,不过哦,他很有特点,它比上面更适合在高并发场景下,写的次数大于读的次数!

多线程要保证线程安全可是非常重要的,而且上面的方法多次提到了CAS,我们必须的聊聊了!(可能前奏有点多,但是上面知识可能会在工作中常用,所以就放到前面了!)


先简单介绍下它,CAS其实就是compare and swap的缩写,比较并交换,他是一种乐观锁,也是一种无锁,认为大概率是不需要加锁的,如果需要加锁就进行比较并交换!

接下来我们说下原理,他又三个参数v,o,nv表示内存中实际存放的值,o表示预期的值,n表示要修改的值,如果v==o,那么我们则把值改成n,如果不相等则更改失败,返回o!(就是这么简单,别惊讶!JUC包下的类,大部分都调用的是unsafe的cas方法,可能参数或多或少,基本上都是这个的变种,八九不离十!)


我们已经了解了它并且还知道了它的原理,但是大家有没有一种疑惑!假如又两个线程AB,A线程发现v==o,准备更改为n,但是B线程来了,也发现v==o准备改成另外一个值,这不并没有实现线程安全嘛!这不吹牛那?

是这样的,自从JNI的出现,我们java也可以越过JVM,调用操作系统原语了,而CAS就是一种系统原语,而系统原语就是属于操作系统原语了,操作系统原语可能是若干条指令,但是一个原语是不能被其他终端打断的,所以并不会出现上面的问题!


最后的最后我们再聊聊CAS有什么优缺点把!

优点:采用无锁的方式,在性能上可能会有不小的提升

缺点:1、会造成ABA问题,2、只能保证一个共享变量(不过可以进行变量合并成对象来玩)


最后的最后的最后在聊聊ABA问题!

臭名昭著的ABA问题,即第一个线程把A把值改成B,然后在改成A,第二个线程发现没有符合期望值,那执行呗!

其实这不完全算问题,有些工作场景并不关心这个,只要最后的值相同就行!比如买了一张火车票,然后秒退,这时候火车票总数不变,不影响!但是有些场景很注重次数,可能这个就很严重了,假如高考试卷,有个人偷偷打开了保险箱,把试题拍了下来,第二个人来的时候是看不出来的,但是这样就造成了泄题!

这时候我们其实加个版本号就行了,每次操作算一个版本,比较值的同时,在比较版本号就行了,具体实现的类有AtomicStampedReference等等!

参考博客:一文彻底搞懂CAS实现原理_东升的思考-CSDN博客

参考博客:CAS是什么?彻底搞懂CAS_阿杰-CSDN博客_cas

以上只是我的简单理解,如果不足之处,请大家指出!

以上是关于CAS了解一下的主要内容,如果未能解决你的问题,请参考以下文章

面试被问到CAS原理,触及知识盲区,脸都绿了!

CAS

你真的了解synchronized和volatile吗?

单点登录之CAS简介

根据CAS协议写的简单的SSO框架

了解CAS