학생들이 이런 인터뷰를 해본 적이 있는지 궁금합니다.
인터뷰어: 동시성 CAS 메커니즘에 대해 이야기해 주세요
샤오밍: CAS, 들어본 적 있는 것 같죠? 에 대해 (뇌 생각이 빠르게 진행됨)2분이 지났습니다...
공기가 죽음처럼 조용합니다...면접관은 가만히 있지 못하고 목을 가다듬었습니다 : 에헴... 그게, 알 수 있나요? 나 잠깐?
샤오밍이 순진하게 웃었다: 에헤헤 잊어버린 것 같은데...
인터뷰어: 아, 괜찮아 오늘 인터뷰는 여기까지고 돌아가서 알림 기다리자
샤오밍이 허탈하게 떠났다...
웃지 마세요. Xiao Ming은 실제로 많은 사람들의 그림자입니다. 인터뷰 과정에서 어색한 대화를 나누는 동급생이 많습니다. 물론 저도 포함됩니다. 사실 이것은 매우 잔인한 현실을 반영합니다. 기초가 탄탄하지 않아요!
그렇다면 면접에서 어떻게 면접관을 이기고 바위처럼 안정될 수 있는지가 문제입니다.
배우세요! 말만 하면 무슨 소용이 있겠습니까? 배워야 하고, 구입한 책을 읽어야 하고, 구입한 강좌를 따라야 합니다. 단지 게임을 하거나 TV 드라마를 따라야 하는 것이 아닙니다. 대머리여야 해!
지금은 베이징 시간 0시 8분입니다. 코드로 글을 쓰고 있는데, 여러분은 어떠세요?
우리는 일상 업무에서 동시성을 자주 다루며 이는 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
进行累加10000次的操作,如果是同步执行的话,inc最终的值应该是1000000,但我们知道在多线程中,程序是并发执行的,也就是说不同的线程可能会同时读取到主内存相同的值,比如这样的场景:
这就是一个很典型的多线程并发修改共享变量带来的问题,那么很显然,它的运行结果也如我们分析的那样,某些情况下达不到1000000:
执行完毕,inc的值为:962370
有些人说通过volatile
关键字可以解决这个问题,因为volatile可以保证线程之间的可见性,也就是说线程可以读取到主内存最新的变量值,然后对其进行操作。
注意了,volatile只能保证线程的可见性
,而不能保证线程操作的原子性
,虽然线程读取到了主内存的inc的最新值,但是 读取
、inc+1
、写入主内存
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); } }
가시성 </ code>이며 스레드 작업을 보장할 수 없습니다<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);글꼴 계열: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">원자성
, 스레드가 주 메모리에서 inc의 최신 값을 읽었지만 읽기
, inc+1< /code>, <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);">메인 메모리에 쓰기< /code>는 3단계 작업이므로 휘발성은 공유 변수의 스레드 안전 문제를 해결할 수 없습니다. 🎜🎜이 문제를 해결하는 방법은 무엇입니까? 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><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><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><div class="contentsignin">로그인 후 복사</div></div><div class="contentsignin">로그인 후 복사</div></div><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가 무엇인지, 무엇인지 생각해 볼 수 있기를 바랍니다. 장점과 단점은 무엇입니까? 구현 방법은 무엇입니까? 물론, 동시성은 매우 큰 개념입니다. 여기서는 작은 지식 포인트 중 하나를 언급하고 내 자신의 학습 경험을 제공하는 간단한 소개입니다. 제대로 설명되지 않은 부분이나 잘못된 부분이 있으면 비밀댓글로 함께 논의해주시면 감사하겠습니다!
저는 프로그래머 Qing Ge입니다. 자신을 발전시키고 대기업으로 진출하고 싶은 학생들은 제 공식 계정을 주목해주세요: Java Study Guide , 여기서는 매일 실제 인터뷰를 바탕으로 Java 관련 지식을 배우고 요약하여 기술 스택을 확장하고 개인 역량을 향상시킬 수 있도록 도와드립니다. 다음에 또 만나요~
위 내용은 실제 인터뷰 질문 : 동시성 CAS 메커니즘에 대해 이야기해주세요의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!