実際の面接の質問: 同時に実行される CAS メカニズムについて話してください。

リリース: 2023-07-26 15:05:54
転載
788 人が閲覧しました

学生がそのような面接を経験したことがあるかどうかはわかりません:

面接官 : CAS メカニズムについて並行して話してください
Xiao Ming : そうですね、CAS さんですね? 聞いたことがあるような気がします... 考えさせてください (脳が高速で考えています)

2 分が経過しました...
空気.

インタビュアーはじっと座っていられず咳払いをした: そうですね... そうですね、簡単に教えていただけますか?
Xiao Minghanhanyixiao: へへ、忘れたようです...
インタビュアー: ああ、関係ありません。今日のインタビューはこれで終わりです。戻ってください。知らせてください
シャオ・ミンは落胆して立ち去りました...


笑わないでください、シャオ・ミンは実際には多くの人々の影であり、たくさんの人々がいます「面接中にぎこちない会話をしたクラスメート。もちろん私もその中に含まれています。実は、これは非常に残酷な現実を反映しています。 基礎がしっかりしていないのです。」

そこで問題は、面接中に面接官を打ち負かし、岩のように安定するにはどうすればよいかということです。 ############学ぶ!ただ話すだけで何の役に立つのですか? 学ばなければなりません、買った本を読まなければなりません、買ったコースに従わなければなりません。ただゲームをしてテレビドラマを追いかけるだけではありません。強くなりたいなら、あなたはそうする必要があります。ハゲなければならない!
現在北京時間 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 が 10,000 回累積されます。同期実行すると、inc の最終値は 1,000,000 になるはずですが、マルチスレッドでは、プログラムは同時に実行されます。つまり、次のシナリオのように、異なるスレッドがメイン メモリから同じ値を同時に読み取る可能性があります。ある瞬間、メインメモリの inc 値が 1000 として読み取られ、自身のワーキングメモリ 1 では inc が 1001 になり、

  • スレッド B が同時にそれを読み出しました。メインメモリの値は 1000 で、自身の作業メモリにも inc 値 1 を設定し、inc は 1001 になります。
  • 彼らは、inc 値をメインメモリに書き込みたいと考えています。同期制御がないため、作業メモリの 1001 をメイン メモリに書き込む可能性があります;
  • その場合、メイン メモリが 2 つの 1 操作を実行していることは明らかです。実際の結果は 1 回だけで、1001 になりました。
  • これは、共有変数を同時に変更するマルチスレッドによって引き起こされる非常に典型的な問題です。明らかに、その実行結果は私たちが分析したとおりですが、場合によってはそれを達成できないこともあります。1000000:
    执行完毕,inc的值为:962370
    ログイン後にコピー
  • 一部の人は、
volatile

キーワードを使用することでこの問題を解決できると主張していますが、これは、volatile はスレッド間の可視性を保証できるため、つまりスレッドがメイン メモリ内の最新データを読み取ることができるためです。そしてそれを操作します。

volatile はスレッドの 可視性 のみを保証できますが、スレッド操作の

原子性

は保証できないことに注意してください。スレッドはメイン メモリ値の最新の inc を読み取っていますが、 , ただし、reading,inc 1,write to mainmemory は 3 段階の操作であるため、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 とは何か、長所と短所、およびそれらを実装する方法は何ですか。 もちろん、同時実行性は非常に大きな概念です。ここでは、いくつかのアイデアを示し、小さな知識ポイントの 1 つについて言及し、私自身の学習経験をいくつか紹介します。 適切に説明されていない点や間違っている点がある場合は、プライベート メッセージを送って一緒に話し合ってください。ありがとうございます。

私は プログラマー Qingge です。ここでのインタビューの質問はこれで終わりです。自分自身を改善して、次のレベルに進みたいと思っています。学生工場内の皆さんは私の公式アカウント Java 学習ガイド に注目してください。ここでは、毎日実際のインタビューに基づいて Java 関連の知識を学び、まとめ、あなたをサポートします。スキルを伸ばし、積み重ねて個人の力を高めます。また次回~

以上が実際の面接の質問: 同時に実行される CAS メカニズムについて話してください。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
cas
ソース:Java学习指南
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!