学生がそのような面接を経験したことがあるかどうかはわかりません:
面接官 : CAS メカニズムについて並行して話してください
Xiao Ming : そうですね、CAS さんですね? 聞いたことがあるような気がします... 考えさせてください (脳が高速で考えています)2 分が経過しました...
空気.インタビュアーはじっと座っていられず咳払いをした: そうですね... そうですね、簡単に教えていただけますか?
Xiao Minghanhanyixiao: へへ、忘れたようです...
インタビュアー: ああ、関係ありません。今日のインタビューはこれで終わりです。戻ってください。知らせてください
シャオ・ミンは落胆して立ち去りました...
笑わないでください、シャオ・ミンは実際には多くの人々の影であり、たくさんの人々がいます「面接中にぎこちない会話をしたクラスメート。もちろん私もその中に含まれています。実は、これは非常に残酷な現実を反映しています。 基礎がしっかりしていないのです。」
そこで問題は、面接中に面接官を打ち負かし、岩のように安定するにはどうすればよいかということです。 ############学ぶ!ただ話すだけで何の役に立つのですか? 学ばなければなりません、買った本を読まなければなりません、買ったコースに従わなければなりません。ただゲームをしてテレビドラマを追いかけるだけではありません。強くなりたいなら、あなたはそうする必要があります。ハゲなければならない!
スレッド セーフとは何かについて説明するための小さな例
現在北京時間 0:08 です。私はコードで記事を書いています。どうですか?
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 になり、
执行完毕,inc的值为:962370
キーワードを使用することでこの問題を解決できると主張していますが、これは、volatile はスレッド間の可視性を保証できるため、つまりスレッドがメイン メモリ内の最新データを読み取ることができるためです。そしてそれを操作します。
volatile はスレッドの 可視性
のみを保証できますが、スレッド操作の
は保証できないことに注意してください。スレッドはメイン メモリ値の最新の inc を読み取っていますが、 , ただし、reading
,inc 1
,write to mainmemory
は 3 段階の操作であるため、volatile では共有変数のスレッド セーフの問題を解決できません。 それでは、この問題をどうやって解決すればいいのでしょうか? Java は次のソリューションを提供します。
<h2 data-tool="mdnice编辑器" style="margin-top: 30px;margin-bottom: 15px;font-weight: bold;border-bottom: 2px solid rgb(239, 112, 96);font-size: 1.3em;">
<span style="display: inline-block;background: rgb(239, 112, 96);color: rgb(255, 255, 255);padding: 3px 10px 1px;border-top-right-radius: 3px;border-top-left-radius: 3px;margin-right: 3px;">几种保证线程安全的方案</span><span style="display: inline-block;vertical-align: bottom;border-bottom: 36px solid #efebe9;border-right: 20px solid transparent;"> </span>
</h2>
<h3 data-tool="mdnice编辑器" style="margin-top: 30px;margin-bottom: 15px;font-weight: bold;font-size: 20px;">1. 通过synchronized关键字实现同步:</h3><pre class="brush:php;toolbar:false;">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);
}
}</pre><p data-tool="mdnice编辑器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">在上面的代码中,我们给 <code style='font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);'>inc ++
外面加了一层代码,使用 synchronized
设置类锁
,保证了代码的同步执行,这是一种基于JVM自身的机制来保障线程的安全性,如果在并发量比较大的情况下,synchronized 会升级为重量级的锁,效率很低。synchronized无法获取当前线程的锁状态,发生异常的情况下会自动解锁,但是如果线程发生阻塞,它是不会释放锁的
执行结果:
执行完毕,inc的值为:1000000
可以看到,这种方式是可以保证线程安全的。
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
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
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的意思是比较与交换(Compare And Swap),它是乐观锁的一种实现机制。
什么是乐观锁?通俗的来说就是它比较乐观,每次在修改变量的值之前不认为别的线程会修改变量,每次都会尝试去获得锁,如果获取失败了,它也会一直等待,直到获取锁为止。说白了,它就是打不死的小强。
而悲观锁呢,顾名思义,就比较悲观了,每次在修改变量前都会认为别人会动这个变量,所以它会把变量锁起来,独占,直到自己修改完毕才会释放锁。说白了,就是比较自私,把好东西藏起来自己偷偷享用,完事了再拿出来给别人。像之前的synchronized
关键字就是悲观锁的一种实现。
CAS是一种无锁原子算法,它的操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。仅当 V值等于A值时,才会将V的值设为B,如果V值和A值不同,则说明已经有其他线程做了更新,则当前线程继续循环等待。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。
在Java中,JUC的atomic包下提供了大量基于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 に関する共有はこれで終わりです。 Java プログラミングの基礎である同時実行性は、非常に重要な知識ポイントです。学生がこの側面をあまり理解していない場合は、この記事を読んだ後、自分でコードを入力して考えられるようにしてください。 CAS とは何か、長所と短所、およびそれらを実装する方法は何ですか。 もちろん、同時実行性は非常に大きな概念です。ここでは、いくつかのアイデアを示し、小さな知識ポイントの 1 つについて言及し、私自身の学習経験をいくつか紹介します。 適切に説明されていない点や間違っている点がある場合は、プライベート メッセージを送って一緒に話し合ってください。ありがとうございます。
私は プログラマー Qingge です。ここでのインタビューの質問はこれで終わりです。自分自身を改善して、次のレベルに進みたいと思っています。学生工場内の皆さんは私の公式アカウント Java 学習ガイド に注目してください。ここでは、毎日実際のインタビューに基づいて Java 関連の知識を学び、まとめ、あなたをサポートします。スキルを伸ばし、積み重ねて個人の力を高めます。また次回~
以上が実際の面接の質問: 同時に実行される CAS メカニズムについて話してください。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。