搜索
首页JavaJava面试题面试真题:请你聊聊并发中的CAS 机制

不知道同学们有没有经历过这样的面试:

面试官:请你聊聊并发中的CAS机制吧
小明:嗯,CAS是吧,好像听说过...我想想哈(大脑飞速思考)

 2分钟过去了...
 空气是死一般的沉静...

面试官坐不住了,清了清嗓子:咳咳... 那个,能简单说说吗?
小明憨憨一笑:嘿嘿,我好像忘记了...
面试官:哦,没关系,今天的面试就到这吧,你回去等通知吧
小明垂头丧气地离开了...


别笑,小明其实是很多人的影子,在面试过程中尬聊的同学不在少数,当然我也包括在内,其实这反映出一个很残酷的现实:基础不扎实!

那么问题来了,如何在面试中吊打面试官,稳如磐石?

学呀!光说有啥用,你得学啊,买的书你得看啊,买的课你得跟着练啊,别光打游戏追剧了,想要变强,唯有秃头!

现在是北京时间0:08,我在码字写文章,你呢?

面试真题:请你聊聊并发中的CAS 机制

一个小例子说说什么是线程安全

并发是Java编程的基础,在我们日常的工作中,很多时候都会跟并发打交道,当然,这也是面试考察的重点。在并发编程中,被提起最多的概念是线程安全,下面我们先来看一段代码,看看运行后会发生什么:

public class Test {
    private static int inc = 0;

    public static void main(String[] args) {
     // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);
        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
             // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                    inc++;
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 运行完毕,期望获取的结果是 1000000
        System.out.println("执行完毕,inc的值为:" + inc);
    }
}

程序中,我创建了100个线程,每个线程中对共享变量inc进行累加10000次的操作,如果是同步执行的话,inc最终的值应该是1000000,但我们知道在多线程中,程序是并发执行的,也就是说不同的线程可能会同时读取到主内存相同的值,比如这样的场景:

  • 线程A在某一个瞬间读取了主内存的inc值为1000,它在自己的工作内存 +1,inc变成了1001;
  • 线程B在同样的瞬间读取到了主内存的inc值为1000,它也在自己的工作内存中对inc的值 +1, inc变成了1001;
  • 他们要往主内存写入inc的值的时候并没有做任何的同步控制,所以他们都有可能把自己工作内存的1001写入到主内存;
  • 那么很显然主内存在进行两次 +1 操作后,实际的结果只进行了一次 +1,变成了1001。

这就是一个很典型的多线程并发修改共享变量带来的问题,那么很显然,它的运行结果也如我们分析的那样,某些情况下达不到1000000:

执行完毕,inc的值为:962370

有些人说通过volatile关键字可以解决这个问题,因为volatile可以保证线程之间的可见性,也就是说线程可以读取到主内存最新的变量值,然后对其进行操作。

注意了,volatile只能保证线程的可见性,而不能保证线程操作的原子性,虽然线程读取到了主内存的inc的最新值,但是 读取inc+1写入主内存 是三步操作,所以volatile无法解决共享变量线程安全的问题。

那么如何解决这个问题?Java为我们提供下面几种解决方案。

几种保证线程安全的方案

1. 通过synchronized关键字实现同步:

public class Test {
    private static int inc = 0;

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);
        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                 // 设置同步机制,让inc按照顺序执行
                    synchronized (Test.class) {
                        inc++;
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc);
    }
}

在上面的代码中,我们给 inc ++ 外面加了一层代码,使用 synchronized 设置类锁,保证了代码的同步执行,这是一种基于JVM自身的机制来保障线程的安全性,如果在并发量比较大的情况下,synchronized 会升级为重量级的锁,效率很低。synchronized无法获取当前线程的锁状态,发生异常的情况下会自动解锁,但是如果线程发生阻塞,它是不会释放锁的

执行结果:

执行完毕,inc的值为:1000000

可以看到,这种方式是可以保证线程安全的。

2. 通过Lock锁实现同步

public class Test {
    private static int inc = 0;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);

        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                 // 设置锁
                    lock.lock();
                    try {
                        inc++;
                    } finally {
                     // 解锁
                        lock.unlock();
                    }
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc);
    }
}

ReentrantLock的底层是通过AQS + CAS来实现的,在并发量比较小的情况下,它的性能不如 synchronized,但是随着并发量的增大,它的性能会越来越好,达到一定量级会完全碾压synchronized。并且Lock是可以尝试获取锁的,它通过代码手动去控制解锁,这点需要格外注意。

执行结果:

执行完毕,inc的值为:1000000

3. 使用 Atomic 原子类

public class Test {
    private static AtomicInteger inc = new AtomicInteger();

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);

        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                    inc.getAndAdd(1);
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc.get());
    }
}

AtomicInteger 底层是基于 CAS 的乐观锁实现的,CAS是一种无锁技术,相对于前面的方案,它的效率更高一些,在下面会详细介绍。

执行结果:

执行完毕,inc的值为:1000000

4. 使用 LongAdder 原子类

public class Test {
    private static LongAdder inc = new LongAdder();

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);

        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                    inc.increment();
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc.intValue());
    }
}

LongAdder 原子类在 JDK1.8 中新增的类,其底层也是基于 CAS 机制实现的。适合于高并发场景下,特别是写大于读的场景,相较于 AtomicInteger、AtomicLong 性能更好,代价是消耗更多的空间,以空间换时间。

执行结果:

执行完毕,inc的值为:1000000

CAS理论

讲到现在,终于我们今天的主角要登场了,她就是CAS

CAS的意思是比较与交换(Compare And Swap),它是乐观锁的一种实现机制。

什么是乐观锁?通俗的来说就是它比较乐观,每次在修改变量的值之前不认为别的线程会修改变量,每次都会尝试去获得锁,如果获取失败了,它也会一直等待,直到获取锁为止。说白了,它就是打不死的小强。

而悲观锁呢,顾名思义,就比较悲观了,每次在修改变量前都会认为别人会动这个变量,所以它会把变量锁起来,独占,直到自己修改完毕才会释放锁。说白了,就是比较自私,把好东西藏起来自己偷偷享用,完事了再拿出来给别人。像之前的synchronized关键字就是悲观锁的一种实现。

CAS是一种无锁原子算法,它的操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。仅当 V值等于A值时,才会将V的值设为B,如果V值和A值不同,则说明已经有其他线程做了更新,则当前线程继续循环等待。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。

CAS的实现

在Java中,JUC的atomic包下提供了大量基于CAS实现的原子类:

面试真题:请你聊聊并发中的CAS 机制

我们以AtomicInteger来举例说明。

AtomicInteger类内部通过一个Unsafe类型的静态不可变的变量unsafe来引用Unsafe的实例。

 // setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

然后,AtomicInteger类用value保存自身的数值,并用get()方法对外提供。注意,它的value是使用volatile修饰的,保证了线程的可见性。

private volatile int value;

/**
 * Creates a new AtomicInteger with the given initial value.
 *
 * @param initialValue the initial value
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

/**
 * Gets the current value.
 *
 * @return the current value
 */
public final int get() {
    return value;
}

一路跟踪incrementAndGet方法到的末尾可以看到是一个native的方法:

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

//  getAndAddInt 方法
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;
}

// compareAndSet方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

可以看到其实incrementAndGet内部的原理就是通过compareAndSwapInt调用底层的机器指令不断比较内存旧值和期望的值,如果比较返回false就继续循环比较,如果返回true则将当前的新值赋给内存里的值,本次处理完毕。

由此我们知道,原子类实现的自增操作可以保证原子性的根本原因在于硬件(处理器)的相关指令支持。将语义上需要多步操作的行为通过一条指令来完成,CAS指令可以达到这个目的。

CAS的缺点

  • 作为乐观锁的一种实现,当多线程竞争资源激烈的情况下,多个线程会发生自旋等待,会消耗一定的 CPU资源。
  • CAS不可避免会出现ABA的问题,关于ABA问题的阐释和解决方案,可以参考我的这篇文章:面试官问你:你知道什么是ABA问题吗?


好了,本期关于CAS的分享就到这里结束了。并发作为Java编程的基石,是一个非常重要的知识点,如果同学们对这块的掌握比较薄弱,希望在读完文章后能自己动手敲敲代码,思考一下什么是CAS,有哪些优缺点,实现方式有哪些。当然,并发是一个非常大的概念,这里只是抛砖引玉,提到了其中的一个小知识点,给出了我自己学习的一点心得体会。如果有阐述不到位或者错误的地方,请私信我一起讨论,感谢!

我是程序员青戈,本期的面试问题分享到这里就结束了,想提升自己,进阶大厂的同学一定要关注我的公众号:Java学习指南,在这里每天会从实际的面试出发带你学习和总结Java相关的知识,帮助你扩充技术栈,提升个人实力。我们下期见~

以上是面试真题:请你聊聊并发中的CAS 机制的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:Java学习指南。如有侵权,请联系admin@php.cn删除
通过 CAS(Central Authentication Service) 实现 PHP 安全验证通过 CAS(Central Authentication Service) 实现 PHP 安全验证Jul 24, 2023 pm 12:49 PM

通过CAS(CentralAuthenticationService)实现PHP安全验证随着互联网的快速发展,用户权限管理和身份验证越来越重要。在开发WEB应用程序时,保护用户数据和防止未经授权访问是至关重要的。为了实现这一目标,我们可以使用CAS(CentralAuthenticationService)来进行PHP的安全验证。CAS

java CAS的概念是什么java CAS的概念是什么May 03, 2023 pm 09:34 PM

1、说明当多个线程同时对某个资源进行CAS操作时,只有一个线程成功,但不会堵塞其他线程,其他线程只会收到操作失败的信号。可见CAS其实是一个乐观的锁。2、实例跟随AtomInteger的代码,我们可以发现最终调用的是sum.misc.Unsafe。看看Unsafe这个名字,它是一个不安全的类别,它利用了Java类别和可见性规则中恰到好处的漏洞。为了速度,Unsafe在Java的安全标准上做出了一些妥协。publicfinalnativebooleancompareAndSwapInt(Objec

java的CAS怎么应用java的CAS怎么应用Apr 18, 2023 pm 06:37 PM

CAS解释:CAS(compareandswap),比较并交换。可以解决多线程并行情况下使用锁造成性能损耗的一种机制.CAS操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。一个线程从主内存中得到num值,并对num进行操作,写入值的时候,线程会把第一次取到的num值和主内存中num值进行比较,如果相等,就会将改变后的num写入主内存,如果不相等,则一直循环对比,知道成功为止。CAS产

Java有锁并发、无锁并发和CAS实例分析Java有锁并发、无锁并发和CAS实例分析May 23, 2023 pm 01:34 PM

有锁并发对于大多数程序员(当然我也基本上是其中一员),并发编程几乎就等价于给相关数据结构加上一个锁(Mutex)。比如如果我们需要一个支持并发的栈,那最简单的方法就是给一个单线程的栈加上锁std::sync::Mutex。(加上Arc是为了能让多个线程都拥有栈的所有权)usestd::sync::{Mutex,Arc};#[derive(Clone)]structConcurrentStack{inner:Arc,}implConcurrentStack{pubfnnew()->Self{

CAS与java乐观锁怎么用CAS与java乐观锁怎么用May 01, 2023 pm 08:07 PM

什么是CASCAS是CompareAndSwap,即比较和交换。为什么CAS没有用到锁还能保证并发情况下安全的操作数据呢,名字其实非常直观的表明了CAS的原理,具体修改数据过程如下:用CAS操作数据时,将数据原始值和要修改的值一并传递给方法比较当前目标变量值与传进去的原始值是否相同如果相同,表示目标变量没有被其他线程修改,直接修改目标变量值即可如果目标变量值与原始值不同,那么证明目标变量已经被其他线程修改过,本次CAS修改失败从上述过程可以看到CAS其实保证的是安全的修改数据,但是修改存在失败的

基于springboot怎么搭建CAS Client客户端基于springboot怎么搭建CAS Client客户端May 14, 2023 am 10:46 AM

1.新建springboot项目并引入依赖org.jasig.cas.clientcas-client-support-springboot3.6.22.配置@EnableCasClient注解packagecom.codetiler.demo;importorg.jasig.cas.client.boot.configuration.EnableCasClient;importorg.springframework.boot.SpringApplication;importorg.spring

面试真题:请你聊聊并发中的CAS 机制面试真题:请你聊聊并发中的CAS 机制Jul 26, 2023 pm 03:05 PM

程序中,我创建了100个线程,每个线程中对共享变量inc进行累加10000次的操作,如果是同步执行的话,inc最终的值应该是1000000,但我们知道在多线程中,程序是并发执行的,也就是说不同的线程可能会同时读取到主内存相同的值

面试官问你:你知道什么是ABA问题吗?面试官问你:你知道什么是ABA问题吗?Jul 26, 2023 pm 03:09 PM

本期关于CAS领域的一个经典ABA问题的解析,不知道你在实际的工作中有没有遇到过,但是在面试中这块是并发知识考查的重点。如果你还没接触过此类的问题,我的建议是你自己将上面的代码运行一下

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前By尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
4 周前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
4 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器