首页 > 其他分享 >面试中被问到CAS机制该怎么回答

面试中被问到CAS机制该怎么回答

时间:2023-05-26 11:05:02浏览次数:34  
标签:面试 CAS int 线程 countDownLatch 问到 public inc


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

并发是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是可以尝试获取锁的,它通过代码手动去控制解锁,这点需要格外注意。

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 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。

与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且,线程间的相互影响也非常小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,他要比基于锁的方式拥有更优越的性能。

CAS的实现

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

面试中被问到CAS机制该怎么回答_Java

我们以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问题的阐释和解决方案,可以参考我的这篇文章

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

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


标签:面试,CAS,int,线程,countDownLatch,问到,public,inc
From: https://blog.51cto.com/u_16130732/6354390

相关文章

  • 面试官问你:知道什么是ABA问题吗?
    大家好啊,我是你们的老朋友青戈,我又来分享干货啦......
  • PyTorch-Forecasting一个新的时间序列预测库
    时间序列预测在金融、天气预报、销售预测和需求预测等各个领域发挥着至关重要的作用。PyTorch-forecasting是一个建立在PyTorch之上的开源Python包,专门用于简化和增强时间序列的工作。在本文中我们介绍PyTorch-Forecasting的特性和功能,并进行示例代码演示。完整文章:https://av......
  • kubenetes 面试题汇总1
    什么是Kubernetes?答:Kubernetes是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。它提供了一种简单而有效的方式来管理大规模的容器化应用程序,并且可以在不同的基础设施上运行,包括公共云、私有云和混合云。Kubernetes中的Pod是什么?它有什么作用?答:Pod是Kubernetes......
  • #yyds干货盘点# LeetCode程序员面试金典:路径总和 II
    题目:给你二叉树的根节点root和一个整数目标和targetSum,找出所有从根节点到叶子节点路径总和等于给定目标和的路径。叶子节点是指没有子节点的节点。 示例1:输入:root=[5,4,8,11,null,13,4,7,2,null,null,5,1],targetSum=22输出:[[5,4,11,2],[5,8,4,5]]示例2:输入:root=......
  • #yyds干货盘点# LeetCode程序员面试金典:多数元素
    1.描述:给定一个大小为n的数组 nums,返回其中的多数元素。多数元素是指在数组中出现次数大于 ⌊n/2⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。 示例 1:输入:nums=[3,2,3]输出:3示例 2:输入:nums=[2,2,1,1,1,2,2]输出:22.代码实现:classSolution{......
  • 基于Lucas-Kanade算法的双目图像光流提取matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:      2.算法涉及理论知识概要        1950年,Gibson首先提出了光流的概念,所谓光流就是指图像表现运动的速度。物体在运动的时候之所以能被人眼发现,就是因为当物体运动时,会在人的视网膜上形成一系列的连续变化的......
  • 基于Lucas-Kanade算法的双目图像光流提取matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:2.算法涉及理论知识概要1950年,Gibson首先提出了光流的概念,所谓光流就是指图像表现运动的速度。物体在运动的时候之所以能被人眼发现,就是因为当物体运动时,会在人的视网膜上形成一系列的连续变化的图像,这些变化信息在不同时间,不断的流过眼......
  • 良好的面试体验多来一点!
    本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。了解校招、分享校招知识的学长来了!今天学长在冲浪的时候,发现了大家都觉得暖心的新闻:面试后收到20元车马费。虽然钱不多,但比那些让人大老远跑去的萝卜坑有心多了。今天学长和大家聊聊一场大家都觉得舒心的面试......
  • lucas定理 学习笔记
    lucas定理学习笔记目录lucas定理学习笔记介绍combination题目描述输入格式输出格式样例输入样例1输出样例2分析code扩展lucas介绍lucas定理用于解决形如\(C_n^m\modp(p\inprime)\)的问题。设\(n,m\)用\(p\)进制来表示为:\((n_an_{a-1}\cdotsn_0)_p,(m_am_{a-......
  • Selenium自动化测试面试必备:高频面试题及答案整理
    自动化测试已经成为现代软件测试中不可或缺的一部分。在自动化测试中,Selenium是最受欢迎的工具之一,因为它可以模拟用户与Web应用程序的交互。因此,对于许多测试工程师来说,熟练掌握Selenium框架是非常重要的。如果你正在寻找一份自动化测试工作,那么你可能会被问到一些关于Selenium的......