概述
CAS(Compare-and-Swap),即比較並替換,是一種實現並發算法時常用到的技術,Java並發包中的很多類都使用了CAS技術。CAS也是現在面試經常問的問題,本文將深入的介紹CAS的原理。
案例
介紹CAS之前,我們先來看一個例子。
/** * @author dajun * @date 2019/7/6 */ public class VolatileTest { public static volatile int race = 0; private static final int THREADS_COUNT = 20; public static void increase() { race++; } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(race); } }
這個例子有些網友反饋會進入死循環,我后面也發現了,在IDEA的RUN模式下確實會陷入死循環,通過 Thread.currentThread().getThreadGroup().list(); 代碼可以打印出當前的線程情況如下:
java.lang.ThreadGroup[name=main,maxpri=10] Thread[main,5,main] Thread[Monitor Ctrl-Break,5,main]
可以看到,除了Main方法線程后,還有一個Monitor Ctrl-Break線程,這個線程是IDEA用來監控Ctrl-Break中斷信號的線程。
解決死循環的辦法:如果是IDEA,可以使用DEBUG模式運行就可以,或者使用下面這段代碼。
import java.util.concurrent.CountDownLatch; /** * @author dajun * @date 2019/7/6 */ public class VolatileTest { public static volatile int race = 0; private static final int THREADS_COUNT = 20; private static CountDownLatch countDownLatch = new CountDownLatch(THREADS_COUNT); public static void increase() { race++; } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } countDownLatch.countDown(); } }); threads[i].start(); } countDownLatch.await(); System.out.println(race); } }
CAS是什么?
CAS是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。
CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改為B,否則就什么都不做。整個比較並替換的操作是一個原子操作。
源碼分析
上面源碼分析時,提到最后調用了compareAndSwapInt方法,接着繼續深入探討該方法,該方法在Unsafe中對應的源碼如下

CAS的缺點:
CAS雖然很高效的解決了原子操作問題,但是CAS仍然存在三大問題。
- 循環時間長開銷很大。
- 只能保證一個變量的原子操作。
- ABA問題。
循環時間長開銷很大:
CAS 通常是配合無限循環一起使用的,我們可以看到 getAndAddInt 方法執行時,如果 CAS 失敗,會一直進行嘗試。如果 CAS 長時間一直不成功,可能會給 CPU 帶來很大的開銷。
只能保證一個變量的原子操作:
當對一個變量執行操作時,我們可以使用循環 CAS 的方式來保證原子操作,但是對多個變量操作時,CAS 目前無法直接保證操作的原子性。但是我們可以通過以下兩種辦法來解決:1)使用互斥鎖來保證原子性;2)將多個變量封裝成對象,通過 AtomicReference 來保證原子性。
什么是ABA問題?ABA問題怎么解決?
如果內存地址V初次讀取的值是A,並且在准備賦值的時候檢查到它的值仍然為A,那我們就能說它的值沒有被其他線程改變過了嗎?
如果在這段期間它的值曾經被改成了B,后來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。這個漏洞稱為CAS操作的“ABA”問題。Java並發包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程序並發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。
