1、什麼是CAS
CAS 即 compare and swap 比較並交換, 涉及到三個參數,內存值V, 預期值A, 要更新爲的值B, 拿着預期值A與內存值V比較,相等則符合預期,將內存值V更新爲B, 不相等,則不能更新V。
爲什麼預期值A與內存值V不一樣了呢?
在多線程環境下,對於臨界區的共享資源,所有線程都可以訪問修改,這時爲了保證數據不會發生錯誤,通常會對訪問臨界區資源加鎖,同一時刻最多隻能讓一個線程訪問(獨佔模式下),這樣會讓線程到臨界區時串行執行,加鎖操作可能會導致併發性能降低,而循環CAS可以實現讓多個線程不加鎖去訪問共享資源,卻也可以保證數據正確性。 如 int share = 1,線程A獲取到share的值1,想要將其修改爲2,這時線程B搶先修改share = 3了,線程A這時拿着share =1 預期值與實際內存中已經變爲3的值比較, 不相等,cas失敗,這時就重新獲取最新的share再次更新,需要不斷循環,直到更新成功;這裏可能會存在線程一直在進行循環cas,消耗cpu資源。
cas缺點:
1、存在ABA問題
2、循環cas, 可能會花費大量時間在循環,浪費cpu資源
3、只能更新一個值(也可解決,AtomicReference 原子引用類泛型可指定對象,實現一個對象中包含多個屬性值來解決只能更新一個值的問題)
2、原子類 Atomic
原子類在JUC的atomic包下提供了 AtomicInteger,AtomicBoolean, AtomicLong等基本數據類型原子類,還有可傳泛型的AtomicReference, 以及帶有版本號的 AtomicStampedReference , 可實現對象的原子更新, 其具體是怎樣保證在多線程環境下,不加鎖的情況也可以原子操作, 是其內部藉助了Unsafe類,來保證更新的原子性。
類圖結構如下:
分別用AtomicInteger和 Integer 演示多個線程執行自增操作,是否能夠保證原子性,執行結果是否正確
代碼如下:
/**
* @author zdd
* 2019/12/22 10:47 上午
* Description: 演示AtomicInteger原子類原子操作
*/
public class CasAtomicIntegerTest {
static final Integer THREAD_NUMBER = 10;
static AtomicInteger atomicInteger = new AtomicInteger(0);
static volatile Integer integer = 0;
public static void main(String[] args) throws InterruptedException {
ThreadTask task = new ThreadTask();
Thread[] threads = new Thread[THREAD_NUMBER];
//1,開啓10個線程
for (int j = 0; j < THREAD_NUMBER; j++) {
Thread thread = new Thread(task);
threads[j]= thread;
}
for (Thread thread:threads) {
//開啓線程
thread.start();
//注: join 爲了保證主線程在所有子線程執行完畢後再打印結果,否則主線程就阻塞等待
// thread.join();
}
// 主線程休眠5s, 等待所有子線程執行完畢再打印
TimeUnit.SECONDS.sleep(5);
System.out.println("執行完畢,atomicInteger的值爲: "+ atomicInteger.get());
System.out.println("執行完畢,integer的值爲 : "+ integer);
}
public static void safeIncr() {
atomicInteger.incrementAndGet();
}
public static void unSafeIncr() {
integer ++;
}
static class ThreadTask implements Runnable{
@Override
public void run() {
// 任務體,分別安全和非安全方式自增1000次
for (int i = 0; i < 1000; i++) {
safeIncr();
}
for (int i = 0; i < 1000; i++) {
unSafeIncr();
}
}
}
}
執行結果如下:
疑問: 上文代碼中注,我本想讓主線程調用每個子線程 join方法,保證主線程在所有子線程執行完畢之後再執行打印結果,然而這樣執行導致非安全的Integer自增結果也正確,猜想是在執行join方法,導致這10個子線程排隊有序在執行了? 因此註釋了該行代碼 ,改爲讓主線程休眠幾秒來保證在子線程執行後再打印。
AtomicInteger如何保證原子性,AtomicInteger持有Unsafe對象,其大部分方法是本地方法,底層實現可保證原子操作。
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
來看一下 AtomicInteger 的自增方法 incrementAndGet(),先自增,再返回增加後的值。
代碼如下:
public final int incrementAndGet() {
//調用unsafe的方法
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
繼續看unsafe如何實現
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//1.獲取當前對象的內存中的值A
var5 = this.getIntVolatile(var1, var2);
//2. var1,var2聯合獲取內存中的值V,var5是期望中的值A, var5+var4 是將要更新爲的新值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//3. 更新成功,跳出while循環,返回更新成功時內存中的值(可能下一刻就被其他線程修改)
return var5;
}
執行流程圖如下:
Unsafe 的compareAndSwapInt是本地方法,可原子地執行更新操作,更新成功返回true,否則false
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
3、CAS的ABA問題
什麼是ABA問題?
例如 線程A獲取變量atomicInteger =100, 想要將其修改爲2019 (此時還未修改), 這時線程B搶先進來將atomicInteger先修改爲101,再修改回atomicInteger =100,這時線程A開始去更新atomicInteger的值了,此時預期值和內存值相等,更新成功atomicInteger =2019;但是線程A 並不知道這個值其實已經被人修改過了。
代碼演示如下:
/**
* zdd
* Description: cas的ABA問題
*/
public class CasTest1 {
// static AtomicInteger atomicInteger = new AtomicInteger(100);
/* 這裏使用原子引用類,傳入Integer類型,
* 和AtomicInteger一樣,AtomicReference使用更靈活,泛型可指定任何引用類型。
* 也可用上面註釋代碼
*/
static AtomicReference<Integer> reference = new AtomicReference<>(100);
public static void main(String[] args) {
//1.開啓線程A
new Thread(()-> {
Integer expect = reference.get();
try {
//模擬執行任務,讓線程B搶先修改
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( "執行3s任務後, 修改值是否成功 "+ reference.compareAndSet(expect,2019)+ " 當前值爲: "+ reference.get());
},"A").start();
//2.開啓線程B
new Thread(()-> {
// expect1 =100
Integer expect1 = reference.get();
//1,先修改爲101,再修改回100,產生ABA問題
reference.compareAndSet(expect1,101);
//expect2 =101
Integer expect2 = reference.get();
reference.compareAndSet(expect2, 100);
},"B").start();
}
}
執行結果如下:可見線程A修改成功
A 執行3s任務後, 修改值是否成功:true 當前值爲: 2019
4、ABA問題的解決方式
解決CAS的ABA問題,是參照數據庫樂觀鎖,添加一個版本號,每更新一次,次數+1,就可解決ABA問題了。
AtomicStampedReference
/**
* zdd
* 2019/11/4 6:30 下午
* Description:
*/
public class CasTest1 {
//設置初始值和版本號
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
//2,採用帶有版本號的
new Thread(()-> {
Integer expect = stampedReference.getReference();
int stamp = stampedReference.getStamp();
try {
//休眠3s,讓線程B執行完ABA操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此時 stamp=1,與實際版本號3不等,這裏更新失敗就是stamp沒有獲取到最新的
System.out.println("是否修改成功: "+stampedReference.compareAndSet(expect, 101, stamp, stamp +1));
System.out.println("當前 stamp 值: " + stampedReference.getStamp()+ "當前 reference: " +stampedReference.getReference());
},"A").start();
new Thread(()-> {
Integer expect = stampedReference.getReference();
int stamp = stampedReference.getStamp();
try {
//休眠1s,讓線程A獲取都舊的值和版本號
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1,100 -> 101, 版本號 1-> 2
stampedReference.compareAndSet(expect, 101 , stamp, stamp+1);
//2, 101 ->100, 版本號 2->3
Integer expect2 = stampedReference.getReference();
stampedReference.compareAndSet(expect2, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
},"B").start();
}
}
執行結果如下:
是否修改成功: false
當前 stamp 值: 3 當前 reference: 100
5、利用cas實現自旋鎖
package cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author zdd
* 2019/12/22 9:12 下午
* Description: 利用cas手動實現自旋鎖
*/
public class SpinLockTest {
static AtomicReference<Thread> atomicReference = new AtomicReference<>();
public static void main(String[] args) {
SpinLockTest spinLockTest = new SpinLockTest();
//測試使用自旋鎖,達到同步鎖一樣的效果 ,開啓2個子線程
new Thread(()-> {
spinLockTest.lock();
System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
try {
//休眠3s
TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
spinLockTest.unLock();
},"線程A").start();
new Thread(()-> {
spinLockTest.lock();
System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
try {
//休眠3s
TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
spinLockTest.unLock();
},"線程B").start();
}
public static void lock() {
Thread currentThread = Thread.currentThread();
for (;;) {
boolean flag =atomicReference.compareAndSet(null,currentThread);
//cas更新成功,則跳出循環,否則一直輪詢
if(flag) {
break;
}
}
}
public static void unLock() {
Thread currentThread = Thread.currentThread();
Thread momeryThread = atomicReference.get();
//比較內存中線程對象與當前對象,不等拋出異常,防止未獲取到鎖的線程調用unlock
if(currentThread != momeryThread) {
throw new IllegalMonitorStateException();
}
//釋放鎖
atomicReference.compareAndSet(currentThread,null);
}
}
執行結果如下圖:
6、總結
通過全文,我們可以知道cas的概念,它的優缺點;原子類的使用,內部藉助Unsafe類循環cas更新操作實現無鎖情況下保證原子更新操作,進一步我們能夠自己利用循環cas實現自旋鎖SpinLock,它與同步鎖如ReentrantLock等區別在於自旋鎖是在未獲取到鎖情況,一直在輪詢,線程時非阻塞的,對cpu資源佔用大,適合查詢多修改少場景,併發性能高;同步鎖是未獲取到鎖,阻塞等待,兩者各有適用場景。
道阻且長,且歌且行!
每天一小步,踏踏實實走好腳下的路,文章爲自己學習總結,不復制黏貼,就是想讓自己的知識沉澱一下,也希望與更多的人交流,如有錯誤,請批評指正!