不知道同學們有沒有經歷過這樣的面試:
面試官:請你聊聊並發中的CAS機制吧
#小明:嗯,CAS是吧,好像聽過...我想想哈(大腦飛快思考)2分鐘過去了...
空氣是死一般的沈靜.. .面試官坐不住了,清了清喉嚨:咳... 那個,能簡單說說嗎?
小明憨憨一笑:嘿嘿,我好像忘記了...
面試官:哦,沒關係,今天的面試就到這吧,你回去等通知吧
小明垂頭喪氣地離開了...
別笑,小明其實是很多人的影子,在面試過程中民聊的同學不在少數,當然我也包括在內,其實這反映出一個很殘酷的現實:基礎不紮實!
那麼問題來了,如何在面試中吊打麵試官,穩如磐石?
學呀!光說有啥用,你得學啊,買的書你得看啊,買的課你得跟著練啊,別光打遊戲追劇了,想要變強,唯有禿頭!
現在是北京時間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
進行累加10000次的操作,如果是同步執行的話,inc最終的值應該是1000000,但我們知道在多執行緒中,程式是並發執行的,也就是說不同的執行緒可能會同時讀取到主記憶體相同的值,例如這樣的場景:
這就是一個很典型的多執行緒並發修改共享變數所帶來的問題,那麼很顯然,它的運行結果也如我們分析的那樣,某些情況下達不到1000000:
执行完毕,inc的值为:962370
有些人說透過volatile
關鍵字可以解決這個問題,因為volatile可以保證執行緒之間的可見性,也就是說執行緒可以讀取到主記憶體最新的變數值,然後對其進行操作。
注意了,volatile只能保證線程的可見性
,而不能保證線程操作的原子性
,雖然線程讀取到了主記憶體的inc的最新值,但是讀取
、inc 1
、寫入主記憶體
是三步驟操作,所以volatile無法解決共享變數執行緒安全的問題。
那麼要如何解決這個問題? 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按照顺序执行 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
可以看到,这种方式是可以保证线程安全的。
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,有哪些優缺點,實作方式有哪些。 當然,並發是一個非常大的概念,這裡只是拋磚引玉,提到了其中的一個小知識點,給出了我自己學習的一點心得體會。 如果有闡述不到位或錯誤的地方,請私訊我一起討論,感謝!
我是程式設計師青戈,本期的面試問題分享到這裡就結束了,想提升自己,進階大工廠的同學一定要關注我的公眾號:Java學習指南,這裡每天會從實際的面試出發帶你學習和總結Java相關的知識,幫助你擴充技術棧,提昇個人實力。我們下期見~
以上是面試真題:請你聊聊並發中的CAS 機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!