CAS
CAS(Compare And Swap)是對一種處理器指令的稱呼,中文譯爲:比較並交換。
它需要三個參數:內存地址V、期望的舊值A、要替換的新值B。
它要完成的功能:當且僅當內存地址V的值等於A時,將A替換爲B並返回true,否則什麼也不做直接返回false。
用Java代碼描述,大致如下所示:
/**
* @paramv 內存地址
* @param a 期望舊值
* @param b 替換的新值
* @return
*/
boolean compareAndSwap(V v,A a,B b){
if (v == a){
v = b;
return true;
}
return false;
}
可以看到CAS是一個典型的check-and-act
操作,如果不加鎖很明顯它不是一個原子操作。
JUC下很多類都用到了大量的CAS操作,如:AQS、ConcurrentHashMap、atomic包下的原子變量類。
它們是如何做到在不加鎖的情況下確保多線程安全的呢?
Java中的CAS操作
CAS操作依賴於現代CPU支持的併發原語,換句話說,整個“比較並交換”的過程CPU會保證它的原子性,執行過程中不會因爲時間片用完而被中斷。
因此Java語言本身是無法實現CAS操作的,需要藉助JNI調用本地代碼來實現。
在Java平臺中,利用sun.misc.Unsafe
類來完成CAS操作,查看源碼會發現大多數方法都是被native修飾的,意味這Java需要調用其他語言的代碼才能實現CAS操作。
正如它的名字一樣,Unsafe是一個不安全的類,使用它可以直接操作內存地址、分配堆外內存。使用Unsafe分配的內存GC是不會自動回收的,因此一旦使用不當很容易造成內存泄漏,所以JDK對Unsafe類的使用做了一些限制。
Unsafe構造方法被私有化了,不能直接new實例,提供了一個獲取實例的方法:getUnsafe(),源碼如下:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
要想拿到Unsafe實例,首先會檢查調用類的類加載器是否爲null,否則會拋出異常。
只有被Bootstrap ClassLoader
類加載器加載的類才符合這個條件,意味着開發者自己編寫的類是無法獲取Unsafe實例的,因爲Java壓根就沒打算讓開發者去用它。
雖然不能通過正規渠道去使用Unsafe,但是可以藉助Java的反射機制來獲取。
- 線程不安全的int自增示例:
public class UnsafeIncr {
volatile int i = 0;//volatile能保證可見性,但是無法保證i++的原子性
void incr(){
i++;
}
public static void main(String[] args) throws InterruptedException {
UnsafeIncr incr = new UnsafeIncr();
//10線程 每個線程自增1萬次 期望結果10萬
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 10000; j++) {
incr.incr();
}
}).start();
}
//主線程休眠1s,等待10個線程執行結束
Thread.sleep(1000);
System.out.println(incr.i);
//輸出結果幾乎總是小於10萬
}
}
- 利用CAS實現的線程安全的int自增:
public class CASIncr {
static final Unsafe unsafe;
static final long fieldOffset;
public int index = 0;
static {
try {
//反射獲取Unsafe實例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//計算 index屬性相對於UnsafeDemo類的內存偏移量
fieldOffset = unsafe.objectFieldOffset(CASIncr.class.getField("index"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
//index利用CAS完成自增操作
void incr(){
int old;
do {
//獲取舊值
old = unsafe.getIntVolatile(this, fieldOffset);
//CAS操作 如果內存地址值=old,說明沒有被其他線程修改,將新值替換爲old+1並返回true,否則返回false再次自旋
} while (!unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1));
}
}
利用CAS實現的自增操作是線程安全的,輸出結果總是正確的。
加鎖和CAS
加鎖是保證線程安全功能最強大,也是適用範圍最廣的一種解決方案。
但是僅對於簡單變量的讀寫操作來加鎖,未免顯得有點“殺雞焉用牛刀”。
CAS可以看成是一種樂觀鎖的實現機制,假定不存在多線程競爭,如果變量沒有被其他線程修改過那麼直接寫入成功,否則自旋進行重試,直到成功爲止。
加鎖(不考慮鎖膨脹,指重量級鎖)是一種悲觀鎖機制,認爲肯定會發生數據衝突,通過OS級別的互斥量來保證每次最多隻有一個線程進入臨界區,通過掛起和喚醒線程來保證數據安全。
在單線程下,CAS的效率肯定是最高的,由於沒有線程競爭,每次寫入都會成功,完全不需要重試。
但是在線程競爭比較激烈的情況下,需要進行多次重試才能寫入成功,反而會浪費CPU的性能。
這也就是爲什麼JDK6中加入“自適應自選鎖”的原因,競爭不激烈CAS自旋重試比掛起再喚醒線程的效率高,競爭激烈就直接掛起線程,避免浪費CPU的性能。
性能測試比較
int index = 0,自增一億次,分別使用synchronized和CAS測試,對比耗時:
public class SyncTest {
private int index = 0;
private final int count = 100000000;
private long startTime = System.currentTimeMillis();
synchronized void incr(){
index++;
if (index == count) {
System.out.println(System.currentTimeMillis() - startTime);
System.exit(1);
}
}
}
public class CASTest {
//不用JDK提供的原子類,自己實現
static final Unsafe unsafe;
static final long fieldOffset;
public int index = 0;
private final int count = 100000000;
private long startTime = System.currentTimeMillis();
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
//直接使用unsafe.getAndAddInt()性能更好,這裏爲了更好的理解CAS,故採用compareAndSwapInt()
void incr(){
int oldValue;
do {
//獲取當前值
oldValue = unsafe.getIntVolatile(this, fieldOffset);
//如果內存地址V的值=oldValue,則替換爲(oldValue+1)並返回true,否則什麼也不做直接返回false(說明值已被其他線程修改),繼續自旋。
//依賴於現代CPU提供的併發原語,CPU會保證整個比較並交換的動作的原子性。
} while (!unsafe.compareAndSwapInt(this, fieldOffset, oldValue, oldValue + 1));
if (oldValue == count - 1) {
System.out.println(System.currentTimeMillis() - startTime);
System.exit(1);
}
}
}
線程數 | Sync耗時(ms) | CAS耗時(ms) |
---|---|---|
1 | 2456 | 1092 |
2 | 4183 | 5201 |
5 | 5633 | 8785 |
CAS不同線程數量下的額外開銷
int index = 0,自增一億次,分別測試CAS在不同數量線程的競爭下額外的自旋次數。
public class CAS {
static final Unsafe unsafe;
static final long fieldOffset;
public int index = 0;
private int count = 100000000;
private long startTime = System.currentTimeMillis();//開始時間
private AtomicInteger casCount = new AtomicInteger(0);//統計CAS次數的原子類
static {
try {
//反射拿到Unsafe實例、獲取index屬性的偏移量
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
void incr(){
while (true) {
int old = unsafe.getIntVolatile(this, fieldOffset);
//CAS自增成功,結束自旋
if (unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1)) {
break;
}else {
//統計額外的自旋次數
casCount.incrementAndGet();
}
}
//自增一億次結束,輸出額外的自旋次數和耗時信息
if (index == count) {
System.out.println("額外的自旋次數:"+casCount.get());
System.out.println("耗時:" + (System.currentTimeMillis() - startTime));
System.exit(1);
}
}
public static void main(String[] args) {
CAS cas = new CAS();
for (int i = 0; i < 5; i++) {
new Thread(()->{
while (true) {
cas.incr();
}
}).start();
}
}
}
線程數 | 額外自旋次數 | 耗時(ms) |
---|---|---|
1 | 0 | 1329 |
2 | 51905576 | 6585 |
5 | 147004339 | 11325 |
10 | 218429562 | 11438 |
單線程下,由於不存在競爭關係,每次寫入都會成功,完全不需要自旋重試。
但是隨着多線程競爭的激烈程度的上升,需要自旋重試的次數不斷變多,性能也隨之下降。
只做自增的話直接調用unsafe.getAndAddInt()可以獲得更好的性能,本博客旨在幫助大家更好的理解CAS操作,遂取舊值再調用compareAndSwapInt()。